diff --git a/README.md b/README.md index ac895df05..a7d1de701 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -<p align="center"> + <p align="center"> <img src="https://cloud.githubusercontent.com/assets/1190261/26751376/63f96538-486a-11e7-81cf-5bc83a945207.png" width="220" height="220" alt="Banner" /> </p> @@ -6,8 +6,6 @@ QMUI Android 的设计目的是用于辅助快速搭建一个具备基本设计还原效果的 Android 项目,同时利用自身提供的丰富控件及兼容处理,让开发者能专注于业务需求而无需耗费精力在基础代码的设计上。不管是新项目的创建,或是已有项目的维护,均可使开发效率和项目质量得到大幅度提升。 -官网:[http://qmuiteam.com/android](http://qmuiteam.com/android) - [](https://github.com/QMUI "QMUI Team") [](http://opensource.org/licenses/MIT "Feel free to contribute.") @@ -21,18 +19,12 @@ QMUI Android 的设计目的是用于辅助快速搭建一个具备基本设计 ### 高效的工具方法 提供高效的工具方法,包括设备信息、屏幕信息、键盘管理、状态栏管理等,可以解决各种常见场景并大幅度提升开发效率。 -## 功能列表 -请查看官网的[功能列表](http://qmuiteam.com/android/page/document.html) - ## 支持 Android 版本 -QMUI Android 支持 API Level 19+。 +QMUI Android 支持 API Level 21+。 ## 使用方法 -请查看官网的[开始使用](http://qmuiteam.com/android/page/start.html)。 - -## QMUI Demo APP 安装包下载 -点击链接下载:[http://cdn.qmuiteam.com/download/android/latest](http://cdn.qmuiteam.com/download/android/latest) - -或扫二维码至官网下载: +可以在工程中的 qmuidemo 项目中查看各组件的使用。 - +## 隐私与安全 +1. 框架会调用 android.os.Build 下的字段读取 brand、model 等信息,用于区分不同的设备。 +2. 框架会尝试读取系统设置获取是否是全面屏手势 diff --git a/arch-annotation/build.gradle b/arch-annotation/build.gradle deleted file mode 100644 index 108fa7ab8..000000000 --- a/arch-annotation/build.gradle +++ /dev/null @@ -1,16 +0,0 @@ -apply plugin: 'java-library' - -version = QMUI_ARCH_VERSION - -dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar']) -} - -sourceCompatibility = "1.7" -targetCompatibility = "1.7" - -// deploy -File deployConfig = rootProject.file('gradle/deploy.properties') -if (deployConfig.exists()) { - apply from: rootProject.file('gradle/deploy.gradle') -} diff --git a/arch-annotation/build.gradle.kts b/arch-annotation/build.gradle.kts new file mode 100644 index 000000000..e57f392df --- /dev/null +++ b/arch-annotation/build.gradle.kts @@ -0,0 +1,16 @@ +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 +} \ No newline at end of file diff --git a/arch-annotation/src/main/java/com/qmuiteam/qmui/arch/annotation/ActivityScheme.java b/arch-annotation/src/main/java/com/qmuiteam/qmui/arch/annotation/ActivityScheme.java index 3de1a6a07..d02da91b3 100644 --- a/arch-annotation/src/main/java/com/qmuiteam/qmui/arch/annotation/ActivityScheme.java +++ b/arch-annotation/src/main/java/com/qmuiteam/qmui/arch/annotation/ActivityScheme.java @@ -25,10 +25,14 @@ 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; } diff --git a/arch-annotation/src/main/java/com/qmuiteam/qmui/arch/annotation/FirstFragments.java b/arch-annotation/src/main/java/com/qmuiteam/qmui/arch/annotation/FirstFragments.java deleted file mode 100644 index 3d7ca0e90..000000000 --- a/arch-annotation/src/main/java/com/qmuiteam/qmui/arch/annotation/FirstFragments.java +++ /dev/null @@ -1,17 +0,0 @@ -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; - -/** - * All possibilities for the first fragment loaded after starting the activity - * used for subclasses of QMUIFragmentActivity - * the value must be subclasses of QMUIFragment - */ -@Retention(RetentionPolicy.CLASS) -@Target(ElementType.TYPE) -public @interface FirstFragments { - Class<?>[] value(); -} diff --git a/arch-annotation/src/main/java/com/qmuiteam/qmui/arch/annotation/FragmentContainerParam.java b/arch-annotation/src/main/java/com/qmuiteam/qmui/arch/annotation/FragmentContainerParam.java new file mode 100644 index 000000000..2f16df2b3 --- /dev/null +++ b/arch-annotation/src/main/java/com/qmuiteam/qmui/arch/annotation/FragmentContainerParam.java @@ -0,0 +1,57 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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 {}; +} diff --git a/arch-annotation/src/main/java/com/qmuiteam/qmui/arch/annotation/FragmentScheme.java b/arch-annotation/src/main/java/com/qmuiteam/qmui/arch/annotation/FragmentScheme.java index a1d223b76..61e105dc8 100644 --- a/arch-annotation/src/main/java/com/qmuiteam/qmui/arch/annotation/FragmentScheme.java +++ b/arch-annotation/src/main/java/com/qmuiteam/qmui/arch/annotation/FragmentScheme.java @@ -26,6 +26,8 @@ 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; @@ -34,4 +36,6 @@ String[] keysWithLongValue() default {}; String[] keysWithFloatValue() default {}; String[] keysWithDoubleValue() default {}; + String[] defaultParams() default {}; + Class<?> valueConverter() default void.class; } diff --git a/arch-annotation/src/main/java/com/qmuiteam/qmui/arch/annotation/LatestVisitRecord.java b/arch-annotation/src/main/java/com/qmuiteam/qmui/arch/annotation/LatestVisitRecord.java index 3f4296eee..03b36a009 100644 --- a/arch-annotation/src/main/java/com/qmuiteam/qmui/arch/annotation/LatestVisitRecord.java +++ b/arch-annotation/src/main/java/com/qmuiteam/qmui/arch/annotation/LatestVisitRecord.java @@ -1,3 +1,19 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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; diff --git a/arch-compiler/build.gradle b/arch-compiler/build.gradle deleted file mode 100644 index 8d414a9e1..000000000 --- a/arch-compiler/build.gradle +++ /dev/null @@ -1,20 +0,0 @@ -apply plugin: 'java-library' - -version = QMUI_ARCH_VERSION - -dependencies { - compile fileTree(dir: 'libs', include: ['*.jar']) - implementation project(':arch-annotation') - implementation 'com.squareup:javapoet:1.10.0' - implementation 'com.google.auto.service:auto-service:1.0-rc2' - annotationProcessor 'com.google.auto.service:auto-service:1.0-rc2' -} - -sourceCompatibility = "1.7" -targetCompatibility = "1.7" - -// deploy -File deployConfig = rootProject.file('gradle/deploy.properties') -if (deployConfig.exists()) { - apply from: rootProject.file('gradle/deploy.gradle') -} diff --git a/arch-compiler/build.gradle.kts b/arch-compiler/build.gradle.kts new file mode 100644 index 000000000..da860209d --- /dev/null +++ b/arch-compiler/build.gradle.kts @@ -0,0 +1,21 @@ +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 +} \ No newline at end of file diff --git a/arch-compiler/src/main/java/com/qmuiteam/qmui/arch/FirstFragmentProcessor.java b/arch-compiler/src/main/java/com/qmuiteam/qmui/arch/FirstFragmentProcessor.java deleted file mode 100644 index 992157443..000000000 --- a/arch-compiler/src/main/java/com/qmuiteam/qmui/arch/FirstFragmentProcessor.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.FirstFragments; -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.LinkedHashSet; -import java.util.List; -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.DeclaredType; -import javax.lang.model.type.MirroredTypesException; -import javax.lang.model.type.TypeMirror; - -@AutoService(Processor.class) -public class FirstFragmentProcessor extends BaseProcessor { - - private static final String FinderSuffix = "_FragmentFinder"; - - private static ClassName FirstFragmentFinderName = ClassName.get( - "com.qmuiteam.qmui.arch.first", "FirstFragmentFinder"); - private static TypeName ClassToIdMapName = ParameterizedTypeName.get(MapName, - QMUIFragmentClassName, IntegerName); - private static TypeName IdToClassName = ParameterizedTypeName.get(MapName, - IntegerName, QMUIFragmentClassName); - - - @Override - public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { - for (Element element : roundEnvironment.getElementsAnnotatedWith(FirstFragments.class)) { - if (element instanceof TypeElement) { - TypeElement classElement = (TypeElement) element; - TypeMirror elementType = classElement.asType(); - TypeElement[] fragments = null; - if (isSubtypeOfType(elementType, QMUI_FRAGMENT_ACTIVITY_TYPE)) { - FirstFragments annotation = element.getAnnotation(FirstFragments.class); - try { - annotation.value(); - } catch (MirroredTypesException mte) { - List<? extends TypeMirror> containerMirrors = mte.getTypeMirrors(); - fragments = new TypeElement[containerMirrors.size()]; - for (int i = 0; i < fragments.length; i++) { - fragments[i] = (TypeElement) ((DeclaredType) containerMirrors.get(i)).asElement(); - } - } - if (fragments == null) { - continue; - } - - processCodeGeneration(classElement, fragments); - } else { - error(element, "Must annotated on subclasses of QMUIFragmentActivity"); - } - } - } - return true; - } - - - private void processCodeGeneration(TypeElement container, TypeElement[] fragments) { - TypeSpec.Builder finderClassBuilder = TypeSpec - .classBuilder(container.getSimpleName() + FinderSuffix) - .addModifiers(Modifier.PUBLIC) - .addSuperinterface(FirstFragmentFinderName); - - FieldSpec classToIdMap = FieldSpec.builder(ClassToIdMapName, "mClassToIdMap") - .addModifiers(Modifier.PRIVATE) - .build(); - - FieldSpec idToClassMap = FieldSpec.builder(IdToClassName, "mIdToClassMap") - .addModifiers(Modifier.PRIVATE) - .build(); - - MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder() - .addModifiers(Modifier.PUBLIC) - .addStatement("mClassToIdMap = new $T<>()", HashMapName) - .addStatement("mIdToClassMap = new $T<>()", HashMapName); - - int currentId = 100; - for (TypeElement element : fragments) { - ClassName elementName = ClassName.get(element); - constructorBuilder.addStatement("mClassToIdMap.put($T.class, $L)", - elementName, - currentId); - constructorBuilder.addStatement("mIdToClassMap.put($L, $T.class)", - currentId, - elementName); - currentId++; - } - - ExecutableElement iGetFragmentClassById = getOverrideMethod( - FirstFragmentFinderName, "getFragmentClassById"); - MethodSpec.Builder getFragmentClassById = MethodSpec.overriding(iGetFragmentClassById) - .addStatement("return mIdToClassMap.get($L)", - iGetFragmentClassById.getParameters().get(0).getSimpleName().toString()); - ExecutableElement iGetIdByFragmentClass = getOverrideMethod( - FirstFragmentFinderName, "getIdByFragmentClass"); - MethodSpec.Builder getIdByFragmentClass = MethodSpec.overriding(iGetIdByFragmentClass) - .addStatement("Integer id = mClassToIdMap.get($L)", iGetIdByFragmentClass.getParameters().get(0).getSimpleName().toString()) - .addStatement("return id != null ? id :$T.NO_ID", FirstFragmentFinderName); - - try { - finderClassBuilder - .addField(classToIdMap) - .addField(idToClassMap) - .addMethod(constructorBuilder.build()) - .addMethod(getFragmentClassById.build()) - .addMethod(getIdByFragmentClass.build()); - JavaFile.builder(container.getQualifiedName().toString().replace("." + container.getSimpleName().toString(), ""), finderClassBuilder.build()) - .build().writeTo(mFiler); - } catch (IOException e) { - error(container, "Unable to write finders for container %s: %s", container.getSimpleName(), e.getMessage()); - } - } - - @Override - public Set<String> getSupportedAnnotationTypes() { - Set<String> types = new LinkedHashSet<>(); - types.add(FirstFragments.class.getCanonicalName()); - return types; - } -} diff --git a/arch-compiler/src/main/java/com/qmuiteam/qmui/arch/SchemeProcessor.java b/arch-compiler/src/main/java/com/qmuiteam/qmui/arch/SchemeProcessor.java index 2d8ffe012..597ba8a0b 100644 --- a/arch-compiler/src/main/java/com/qmuiteam/qmui/arch/SchemeProcessor.java +++ b/arch-compiler/src/main/java/com/qmuiteam/qmui/arch/SchemeProcessor.java @@ -16,7 +16,6 @@ package com.qmuiteam.qmui.arch; import com.google.auto.service.AutoService; -import com.google.common.collect.Lists; import com.qmuiteam.qmui.arch.annotation.ActivityScheme; import com.qmuiteam.qmui.arch.annotation.FragmentScheme; import com.squareup.javapoet.ClassName; @@ -46,7 +45,6 @@ import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; -import javax.lang.model.type.DeclaredType; import javax.lang.model.type.MirroredTypesException; import javax.lang.model.type.TypeMirror; @@ -54,6 +52,8 @@ 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"); @@ -164,12 +164,17 @@ public int compare(Item item, Item t1) { 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") @@ -183,6 +188,12 @@ public int compare(Item item, Item t1) { /*---*/.add(floatParam) /*---*/.add(",") /*---*/.add(doubleParam) + /*---*/.add(",") + /*---*/.add(defaultParam) + /*---*/.add(",") + /*---*/.add(customMatcher) + /*---*/.add(",") + /*---*/.add(valueConverter) /**/.add(")") .add(")") .build(); @@ -201,20 +212,23 @@ public int compare(Item item, Item t1) { 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("$S", annotation.forceNewActivityKey()) - /*---*/.add(",") /*---*/.add("required") /*---*/.add(",") /*---*/.add(intParam) @@ -226,6 +240,12 @@ public int compare(Item item, Item t1) { /*---*/.add(floatParam) /*---*/.add(",") /*---*/.add(doubleParam) + /*---*/.add(",") + /*---*/.add(defaultParam) + /*---*/.add(",") + /*---*/.add(customMatcher) + /*---*/.add(",") + /*---*/.add(valueConverter) /**/.add(")") .add(")") .build(); @@ -239,8 +259,9 @@ public int compare(Item item, Item t1) { ExecutableElement findScheme = getOverrideMethod( SchemeMap, "findScheme"); List<? extends VariableElement> findSchemeParams = findScheme.getParameters(); - String schemeAction = findSchemeParams.get(0).getSimpleName().toString(); - String schemeParam = findSchemeParams.get(1).getSimpleName().toString(); + 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())") @@ -248,7 +269,7 @@ public int compare(Item item, Item t1) { .endControlFlow() .beginControlFlow("for (int i = 0; i < list.size(); i++)") /**/.addStatement("$T item = list.get(i)", SchemeItem) - /**/.beginControlFlow("if(item.match($L))", schemeParam) + /**/.beginControlFlow("if(item.match($L, $L))", schemeHandler, schemeParam) /*--*/.addStatement("return item") /**/.endControlFlow() .endControlFlow() @@ -256,7 +277,7 @@ public int compare(Item item, Item t1) { ExecutableElement exists = getOverrideMethod( SchemeMap, "exists"); MethodSpec.Builder getRecordMetaByClass = MethodSpec.overriding(exists) - .addStatement("return mSchemeMap.containsKey($L)", exists.getParameters().get(0).getSimpleName().toString()); + .addStatement("return mSchemeMap.containsKey($L)", exists.getParameters().get(1).getSimpleName().toString()); classBuilder .addMethod(constructorBuilder.build()) @@ -330,6 +351,32 @@ private CodeBlock generateCustomFactory(boolean isActivity, AnnotationMirror ann 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; diff --git a/arch/build.gradle b/arch/build.gradle deleted file mode 100644 index a3b072a80..000000000 --- a/arch/build.gradle +++ /dev/null @@ -1,40 +0,0 @@ -apply plugin: 'com.android.library' - -version = QMUI_ARCH_VERSION - -android { - - compileSdkVersion parent.ext.compileSdkVersion - lintOptions { - abortOnError false - } - - defaultConfig { - minSdkVersion parent.ext.minSdkVersion - targetSdkVersion parent.ext.targetSdkVersion - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } - -} - -dependencies { - api "androidx.appcompat:appcompat:$appcompatVersion" - api "androidx.fragment:fragment:1.3.0-alpha03" - - api project(':arch-annotation') - compileOnly project(':qmui') - testImplementation "junit:junit:$junitVersion" - lintChecks project(':lintrule') -} - -// deploy -File deployConfig = rootProject.file('gradle/deploy.properties') -if (deployConfig.exists()) { - apply from: rootProject.file('gradle/deploy.gradle') -} diff --git a/arch/build.gradle.kts b/arch/build.gradle.kts new file mode 100644 index 000000000..6b9782c83 --- /dev/null +++ b/arch/build.gradle.kts @@ -0,0 +1,42 @@ +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")) +} \ No newline at end of file diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/InnerBaseActivity.java b/arch/src/main/java/com/qmuiteam/qmui/arch/InnerBaseActivity.java index 6a1de06bf..c7db3c979 100644 --- a/arch/src/main/java/com/qmuiteam/qmui/arch/InnerBaseActivity.java +++ b/arch/src/main/java/com/qmuiteam/qmui/arch/InnerBaseActivity.java @@ -131,13 +131,13 @@ private void checkLatestVisitRecord() { sLatestVisitActivityUUid = mUUid; if (!shouldPerformLatestVisitRecord()) { - QMUILatestVisit.getInstance(this).clearFragmentLatestVisitRecord(); + QMUILatestVisit.getInstance(this).clearActivityLatestVisitRecord(); return; } LatestVisitRecord latestVisitRecord = cls.getAnnotation(LatestVisitRecord.class); if(latestVisitRecord == null || (latestVisitRecord.onlyForDebug() && !QMUIConfig.DEBUG)){ - QMUILatestVisit.getInstance(this).clearFragmentLatestVisitRecord(); + QMUILatestVisit.getInstance(this).clearActivityLatestVisitRecord(); return; } QMUILatestVisit.getInstance(this).performLatestVisitRecord(this); diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/QMUIActivity.java b/arch/src/main/java/com/qmuiteam/qmui/arch/QMUIActivity.java index b891d7537..39530558a 100644 --- a/arch/src/main/java/com/qmuiteam/qmui/arch/QMUIActivity.java +++ b/arch/src/main/java/com/qmuiteam/qmui/arch/QMUIActivity.java @@ -25,11 +25,14 @@ import android.view.ViewGroup; import android.widget.FrameLayout; -import com.qmuiteam.qmui.util.QMUIDisplayHelper; -import com.qmuiteam.qmui.util.QMUIStatusBarHelper; - 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; @@ -41,7 +44,7 @@ import static com.qmuiteam.qmui.arch.SwipeBackLayout.EDGE_RIGHT; import static com.qmuiteam.qmui.arch.SwipeBackLayout.EDGE_TOP; -public class QMUIActivity extends InnerBaseActivity { +public class QMUIActivity extends InnerBaseActivity implements ActivitySchemeRefreshable { private static final String TAG = "QMUIActivity"; private SwipeBackLayout.ListenerRemover mListenerRemover; private SwipeBackgroundView mSwipeBackgroundView; @@ -75,7 +78,7 @@ public void onScroll(int dragDirection, int movingEdge, float scrollPercent) { scrollPercent = Math.max(0f, Math.min(1f, scrollPercent)); int targetOffset = (int) (Math.abs(backViewInitOffset( QMUIActivity.this, dragDirection, movingEdge)) * (1 - scrollPercent)); - SwipeBackLayout.offsetInSwipeBack(mSwipeBackgroundView, movingEdge, targetOffset); + SwipeBackLayout.translateInSwipeBack(mSwipeBackgroundView, movingEdge, targetOffset); } } @@ -87,16 +90,19 @@ public void onSwipeBackBegin(int dragDirection, int moveEdge) { 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); + 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.offsetInSwipeBack(mSwipeBackgroundView, moveEdge, + SwipeBackLayout.translateInSwipeBack(mSwipeBackgroundView, moveEdge, Math.abs(backViewInitOffset(decorView.getContext(), dragDirection, moveEdge))); } } @@ -112,11 +118,21 @@ public void onScrollOverThreshold() { public int getDragDirection(SwipeBackLayout swipeBackLayout, SwipeBackLayout.ViewMoveAction moveAction, float downX, float downY, float dx, float dy, float touchSlop) { - if(!QMUISwipeBackActivityManager.getInstance().canSwipeBack()){ + 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 @@ -137,11 +153,12 @@ public void setContentView(View view) { @Override public void setContentView(int layoutResID) { SwipeBackLayout swipeBackLayout = SwipeBackLayout.wrap(this, layoutResID, dragViewMoveAction(), mSwipeCallback); - if (translucentFull()) { - swipeBackLayout.getContentView().setFitsSystemWindows(false); - } else { - swipeBackLayout.getContentView().setFitsSystemWindows(true); - } + swipeBackLayout.setOnInsetsHandler(new SwipeBackLayout.OnInsetsHandler() { + @Override + public int getInsetsType() { + return getRootViewInsetsType(); + } + }); mListenerRemover = swipeBackLayout.addSwipeListener(mSwipeListener); super.setContentView(swipeBackLayout); } @@ -152,12 +169,13 @@ public void setContentView(View view, ViewGroup.LayoutParams params) { } private View newSwipeBackLayout(View view) { - if (translucentFull()) { - view.setFitsSystemWindows(false); - } else { - view.setFitsSystemWindows(true); - } 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; } @@ -188,10 +206,18 @@ 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 * @@ -290,15 +316,6 @@ protected SwipeBackLayout.ViewMoveAction dragViewMoveAction() { return SwipeBackLayout.MOVE_VIEW_AUTO; } - /** - * Immersive processing - * - * @return if true, the area under status bar belongs to content; otherwise it belongs to padding - */ - protected boolean translucentFull() { - return false; - } - /** * restore sub window(e.g dialog) when drag back to previous activity * @@ -318,9 +335,14 @@ public Intent onLastActivityFinish() { return null; } + @WindowInsetsCompat.Type.InsetsType + public int getRootViewInsetsType() { + return WindowInsetsCompat.Type.ime(); + } + @Override public void finish() { - if (!QMUISwipeBackActivityManager.getInstance().canSwipeBack()) { + if (isTaskRoot()) { Intent intent = onLastActivityFinish(); if (intent != null) { startActivity(intent); @@ -328,4 +350,9 @@ public void finish() { } super.finish(); } + + @Override + public void refreshFromScheme(@Nullable Intent intent) { + + } } diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/QMUIFragment.java b/arch/src/main/java/com/qmuiteam/qmui/arch/QMUIFragment.java index 74042be55..1af19c75b 100644 --- a/arch/src/main/java/com/qmuiteam/qmui/arch/QMUIFragment.java +++ b/arch/src/main/java/com/qmuiteam/qmui/arch/QMUIFragment.java @@ -16,6 +16,19 @@ 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; @@ -26,10 +39,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.view.ViewParent; -import android.view.animation.AlphaAnimation; import android.view.animation.Animation; -import android.view.animation.AnimationUtils; import android.widget.FrameLayout; import androidx.activity.OnBackPressedCallback; @@ -37,7 +47,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.arch.core.util.Function; -import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentContainerView; @@ -45,10 +55,12 @@ 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 androidx.viewpager.widget.ViewPager; -import androidx.viewpager2.widget.ViewPager2; import com.qmuiteam.qmui.QMUIConfig; import com.qmuiteam.qmui.QMUILog; @@ -61,10 +73,10 @@ 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.util.QMUIViewHelper; import com.qmuiteam.qmui.widget.QMUITopBar; import java.lang.reflect.Field; @@ -72,16 +84,6 @@ import java.util.List; import java.util.concurrent.atomic.AtomicInteger; -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; - /** * With the use of {@link QMUIFragmentActivity}, {@link QMUIFragment} brings more features, * such as swipe back, transition config, and so on. @@ -89,17 +91,23 @@ * Created by cgspine on 15/9/14. */ public abstract class QMUIFragment extends Fragment implements - QMUIFragmentLazyLifecycleOwner.Callback, LatestVisitArgumentCollector { + 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.anim.slide_in_right, R.anim.slide_out_left, - R.anim.slide_in_left, R.anim.slide_out_right); + 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.anim.scale_enter, R.anim.slide_still, - R.anim.slide_still, R.anim.scale_exit); + 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; @@ -109,7 +117,7 @@ public abstract class QMUIFragment extends Fragment implements 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); @@ -120,14 +128,18 @@ public abstract class QMUIFragment extends Fragment implements private int mTargetRequestCode = NO_REQUEST_CODE; private View mBaseView; - private SwipeBackLayout mCacheSwipeBackLayout; 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<Boolean> isInEnterAnimationLiveData = new MutableLiveData<>(false); private boolean mCalled = true; private ArrayList<Runnable> mDelayRenderRunnableList; private ArrayList<Runnable> mPostResumeRunnableList; @@ -145,13 +157,17 @@ public void run() { } } }; - private QMUIFragmentLazyLifecycleOwner mLazyViewLifecycleOwner; 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(); } }; @@ -193,27 +209,59 @@ 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)) { @@ -282,20 +330,20 @@ public <T extends Effect> void notifyEffect(T effect) { private void ensureFragmentEffectRegistry() { if (mFragmentEffectRegistry == null) { - QMUIFragmentContainerProvider provider = findFragmentContainerProvider(); + QMUIFragmentContainerProvider provider = findFragmentContainerProvider(false); ViewModelStoreOwner viewModelStoreOwner = provider != null ? provider.getContainerViewModelStoreOwner() : requireActivity(); mFragmentEffectRegistry = new ViewModelProvider(viewModelStoreOwner).get(QMUIFragmentEffectRegistry.class); } } @Nullable - protected QMUIFragmentContainerProvider findFragmentContainerProvider() { - Fragment parent = getParentFragment(); - while (parent != null) { - if (parent instanceof QMUIFragmentContainerProvider) { - return (QMUIFragmentContainerProvider) parent; + protected QMUIFragmentContainerProvider findFragmentContainerProvider(boolean includeSelf) { + Fragment current = includeSelf ? this : getParentFragment(); + while (current != null) { + if (current instanceof QMUIFragmentContainerProvider) { + return (QMUIFragmentContainerProvider) current; } else { - parent = parent.getParentFragment(); + current = current.getParentFragment(); } } Activity activity = getActivity(); @@ -305,7 +353,7 @@ protected QMUIFragmentContainerProvider findFragmentContainerProvider() { return null; } - protected int startFragmentAndDestroyCurrent(QMUIFragment fragment) { + public int startFragmentAndDestroyCurrent(QMUIFragment fragment) { return startFragmentAndDestroyCurrent(fragment, true); } @@ -322,15 +370,15 @@ protected int startFragmentAndDestroyCurrent(QMUIFragment fragment) { * @param useNewTransitionConfigWhenPop if true, use animation generated by transition C->D, * else, use animation generated by transition B->C */ - protected int startFragmentAndDestroyCurrent(QMUIFragment fragment, + public int startFragmentAndDestroyCurrent(QMUIFragment fragment, boolean useNewTransitionConfigWhenPop) { if (!checkStateLoss("startFragmentAndDestroyCurrent")) { return -1; } - QMUIFragmentContainerProvider provider = findFragmentContainerProvider(); + QMUIFragmentContainerProvider provider = findFragmentContainerProvider(true); if (provider == null) { - if (BuildConfig.DEBUG) { + if (QMUIConfig.DEBUG) { throw new RuntimeException("Can not find the fragment container provider."); } else { Log.d(TAG, "Can not find the fragment container provider."); @@ -338,6 +386,10 @@ protected int startFragmentAndDestroyCurrent(QMUIFragment fragment, } } + if(provider.getContainerFragmentManager().isDestroyed()){ + return -1; + } + QMUIFragment.TransitionConfig transitionConfig = fragment.onFetchTransitionConfig(); String tagName = fragment.getClass().getSimpleName(); FragmentManager fragmentManager = provider.getContainerFragmentManager(); @@ -345,6 +397,7 @@ protected int startFragmentAndDestroyCurrent(QMUIFragment fragment, .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); @@ -362,9 +415,9 @@ public int startFragment(QMUIFragment fragment) { if (!checkStateLoss("startFragment")) { return -1; } - QMUIFragmentContainerProvider provider = findFragmentContainerProvider(); + QMUIFragmentContainerProvider provider = findFragmentContainerProvider(true); if (provider == null) { - if (BuildConfig.DEBUG) { + if (QMUIConfig.DEBUG) { throw new RuntimeException("Can not find the fragment container provider."); } else { Log.d(TAG, "Can not find the fragment container provider."); @@ -374,6 +427,54 @@ public int startFragment(QMUIFragment fragment) { 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<FragmentTransaction> 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) @@ -393,9 +494,9 @@ public int startFragmentForResult(QMUIFragment fragment, int requestCode) { if (requestCode == NO_REQUEST_CODE) { throw new RuntimeException("requestCode can not be " + NO_REQUEST_CODE); } - QMUIFragmentContainerProvider provider = findFragmentContainerProvider(); + QMUIFragmentContainerProvider provider = findFragmentContainerProvider(true); if (provider == null) { - if (BuildConfig.DEBUG) { + if (QMUIConfig.DEBUG) { throw new RuntimeException("Can not find the fragment container provider."); } else { Log.d(TAG, "Can not find the fragment container provider."); @@ -410,10 +511,14 @@ public int startFragmentForResult(QMUIFragment fragment, int requestCode) { } 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) @@ -435,18 +540,33 @@ public void setFragmentResult(int resultCode, Intent data) { 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); } - mLazyViewLifecycleOwner = new QMUIFragmentLazyLifecycleOwner(this); - mLazyViewLifecycleOwner.setViewVisible(getUserVisibleHint()); - getViewLifecycleOwner().getLifecycle().addObserver(mLazyViewLifecycleOwner); } 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(); @@ -455,69 +575,55 @@ private SwipeBackLayout newSwipeBackLayout() { if (rootView.getParent() != null) { ((ViewGroup) rootView.getParent()).removeView(rootView); } - rootView.setTag(R.id.qmui_arch_reused_layout, true); } - if (translucentFull()) { - rootView.setFitsSystemWindows(false); - } else { - rootView.setFitsSystemWindows(true); - } - final SwipeBackLayout swipeBackLayout = SwipeBackLayout.wrap(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) { - // 1. can not swipe back if in animation - if (mEnterAnimationStatus != ANIMATION_ENTER_STATUS_END) { - return SwipeBackLayout.DRAG_DIRECTION_NONE; - } - // 2. can not swipe back if it is not managed by FragmentContainer - QMUIFragmentContainerProvider provider = findFragmentContainerProvider(); - if (provider == null) { - return SwipeBackLayout.DRAG_DIRECTION_NONE; + mCalled = false; + if(mDisableSwipeBackByMutiStarted){ + return DRAG_DIRECTION_NONE; } - - FragmentManager fragmentManager = provider.getContainerFragmentManager(); - if (fragmentManager == null || fragmentManager != getParentFragmentManager()) { - return SwipeBackLayout.DRAG_DIRECTION_NONE; + boolean canHandle = canHandleSwipeBack(); + if (canHandle && !mCalled) { + throw new RuntimeException(getClass().getSimpleName() + " did not call through to super.shouldPreventSwipeBack()"); } - // 3. need be handled by inner FragmentContainer - if (fragmentManager.getPrimaryNavigationFragment() != null) { - return SwipeBackLayout.DRAG_DIRECTION_NONE; + if(!canHandle){ + return DRAG_DIRECTION_NONE; } - - // 4. can not swipe back if the view is null - View view = getView(); - if (view == null) { - return SwipeBackLayout.DRAG_DIRECTION_NONE; - } - - // 5. can not swipe back if if the Fragment is in ViewPager - ViewParent parent = view.getParent(); - while (parent != null) { - if (parent instanceof ViewPager || parent instanceof ViewPager2) { - return SwipeBackLayout.DRAG_DIRECTION_NONE; - } - parent = parent.getParent(); - } - - // 6. can not swipe back if the backStack entry count is less than 2 - if (fragmentManager.getBackStackEntryCount() <= 1 && - !QMUISwipeBackActivityManager.getInstance().canSwipeBack()) { - return SwipeBackLayout.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); } - return swipeBackLayout; } private SwipeBackLayout.SwipeListener mSwipeListener = new SwipeBackLayout.SwipeListener() { @@ -527,7 +633,7 @@ public int getDragDirection(SwipeBackLayout swipeBackLayout, SwipeBackLayout.Vie @Override public void onScrollStateChange(int state, float scrollPercent) { Log.i(TAG, "SwipeListener:onScrollStateChange: state = " + state + " ;scrollPercent = " + scrollPercent); - QMUIFragmentContainerProvider provider = findFragmentContainerProvider(); + QMUIFragmentContainerProvider provider = findFragmentContainerProvider(false); if (provider == null || provider.getFragmentContainerView() == null) { return; } @@ -540,11 +646,15 @@ public void onScrollStateChange(int state, float scrollPercent) { mSwipeBackgroundView = null; } else if (scrollPercent >= 1.0F) { // unbind mSwipeBackgroundView util onDestroy - if (getActivity() != null) { - popBackStack(); + 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; - getActivity().overridePendingTransition(R.anim.swipe_back_enter, exitAnim); + popBackStack(); + activity.overridePendingTransition(R.anim.swipe_back_enter, exitAnim); + sPopBackWhenSwipeFinished = false; } } return; @@ -593,7 +703,9 @@ public String newTagName() { return null; } }); + sPopBackWhenSwipeFinished = true; popBackStack(); + sPopBackWhenSwipeFinished = false; } } } @@ -601,7 +713,7 @@ public String newTagName() { @Override public void onScroll(int dragDirection, int moveEdge, float scrollPercent) { scrollPercent = Math.max(0f, Math.min(1f, scrollPercent)); - QMUIFragmentContainerProvider provider = findFragmentContainerProvider(); + QMUIFragmentContainerProvider provider = findFragmentContainerProvider(false); if (provider == null || provider.getFragmentContainerView() == null) { return; } @@ -613,11 +725,11 @@ public void onScroll(int dragDirection, int moveEdge, float scrollPercent) { View view = container.getChildAt(i); Object tag = view.getTag(R.id.qmui_arch_swipe_layout_in_back); if (SWIPE_BACK_VIEW.equals(tag)) { - SwipeBackLayout.offsetInSwipeBack(view, moveEdge, targetOffset); + SwipeBackLayout.translateInSwipeBack(view, moveEdge, targetOffset); } } if (mSwipeBackgroundView != null) { - SwipeBackLayout.offsetInSwipeBack(mSwipeBackgroundView, moveEdge, targetOffset); + SwipeBackLayout.translateInSwipeBack(mSwipeBackgroundView, moveEdge, targetOffset); } } @@ -625,7 +737,7 @@ public void onScroll(int dragDirection, int moveEdge, float scrollPercent) { @Override public void onSwipeBackBegin(final int dragDirection, final int moveEdge) { Log.i(TAG, "SwipeListener:onSwipeBackBegin: moveEdge = " + moveEdge); - QMUIFragmentContainerProvider provider = findFragmentContainerProvider(); + QMUIFragmentContainerProvider provider = findFragmentContainerProvider(false); if (provider == null || provider.getFragmentContainerView() == null) { return; } @@ -635,7 +747,7 @@ public void onSwipeBackBegin(final int dragDirection, final int moveEdge) { onDragStart(); FragmentManager fragmentManager = provider.getContainerFragmentManager(); int backStackCount = fragmentManager.getBackStackEntryCount(); - if (backStackCount > 1) { + if (backStackCount > 1 && !mFinishActivityIfOnBackPressed) { Utils.findAndModifyOpInBackStackRecord(fragmentManager, -1, new Utils.OpHandler() { @Override public boolean handle(Object op) { @@ -647,13 +759,6 @@ public boolean handle(Object op) { cmdField.setAccessible(true); int cmd = (int) cmdField.get(op); if (cmd == 3) { - Field popEnterAnimField = Utils.getOpPopEnterAnimField(op); - if (popEnterAnimField != null) { - popEnterAnimField.setAccessible(true); - popEnterAnimField.set(op, 0); - } - - Field fragmentField = Utils.getOpFragmentField(op); if (fragmentField != null) { fragmentField.setAccessible(true); @@ -666,7 +771,7 @@ public boolean handle(Object op) { if (baseView != null) { addViewInSwipeBack(container, baseView, 0); handleChildFragmentListWhenSwipeBackStart(mModifiedFragment, baseView); - SwipeBackLayout.offsetInSwipeBack(baseView, moveEdge, + SwipeBackLayout.translateInSwipeBack(baseView, moveEdge, Math.abs(backViewInitOffset(baseView.getContext(), dragDirection, moveEdge))); } } @@ -694,15 +799,18 @@ public String newTagName() { 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()); + 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.offsetInSwipeBack(mSwipeBackgroundView, moveEdge, + SwipeBackLayout.translateInSwipeBack(mSwipeBackgroundView, moveEdge, Math.abs(backViewInitOffset(decorView.getContext(), dragDirection, moveEdge))); } } @@ -734,6 +842,8 @@ private void removeViewInSwipeBack(ViewGroup parent, Function<View, Void> onRemo if (onRemove != null) { onRemove.apply(view); } + view.setTranslationY(0); + view.setTranslationX(0); parent.removeView(view); } } @@ -820,95 +930,101 @@ public Void apply(View input) { } }; - private boolean canNotUseCacheViewInCreateView() { - return mCacheSwipeBackLayout.getParent() != null || ViewCompat.isAttachedToWindow(mCacheSwipeBackLayout); - } - public boolean isInSwipeBack() { return mIsInSwipeBack; } + protected boolean forceDisableHardwareAcceleratedForSwipeBackground(){ + return false; + } + @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - SwipeBackLayout swipeBackLayout; - if (mCacheSwipeBackLayout == null) { - swipeBackLayout = newSwipeBackLayout(); - mCacheSwipeBackLayout = swipeBackLayout; - } else { - if (canNotUseCacheViewInCreateView()) { - // try removeView first - container.removeView(mCacheSwipeBackLayout); - } - - if (canNotUseCacheViewInCreateView()) { - // give up!!! - Log.i(TAG, "can not use cache swipeBackLayout, this may happen " + - "if invoke popBackStack duration fragment transition"); - mCacheSwipeBackLayout.clearSwipeListeners(); - swipeBackLayout = newSwipeBackLayout(); - mCacheSwipeBackLayout = swipeBackLayout; - } else { - swipeBackLayout = mCacheSwipeBackLayout; - mCacheRootView.setTag(R.id.qmui_arch_reused_layout, true); - } - } - - + SwipeBackLayout swipeBackLayout = newSwipeBackLayout(); if (!isCreateForSwipeBack) { mBaseView = swipeBackLayout.getContentView(); swipeBackLayout.setTag(R.id.qmui_arch_swipe_layout_in_back, null); } swipeBackLayout.setFitsSystemWindows(false); - - if (getActivity() != null) { - QMUIViewHelper.requestApplyInsets(getActivity().getWindow()); - } - return swipeBackLayout; } - protected void onBackPressed() { - QMUIFragmentContainerProvider provider = findFragmentContainerProvider(); - if (!(provider instanceof FragmentActivity) || provider.getContainerFragmentManager() == null || - provider.getContainerFragmentManager().getBackStackEntryCount() > 1) { - // disable this and go with FragmentManager's backPressesCallback - // because it will call execPendingActions before popBackStackImmediate - mOnBackPressedCallback.setEnabled(false); - mOnBackPressedDispatcher.onBackPressed(); - mOnBackPressedCallback.setEnabled(true); - } else { - QMUIFragment.TransitionConfig transitionConfig = onFetchTransitionConfig(); - if (QMUISwipeBackActivityManager.getInstance().canSwipeBack()) { - requireActivity().finish(); - requireActivity().overridePendingTransition(transitionConfig.popenter, transitionConfig.popout); - 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); - requireActivity().overridePendingTransition(transitionConfig.popenter, transitionConfig.popout); - requireActivity().finish(); + 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 { - onHandleSpecLastFragmentFinish(requireActivity(), transitionConfig, toExec); + if(!sPopBackWhenSwipeFinished){ + activity.finishAfterTransition(); + }else{ + activity.finish(); + } + activity.overridePendingTransition(transitionConfig.popenterAnimation, transitionConfig.popoutAnimation); } - } else { - requireActivity().finish(); - requireActivity().overridePendingTransition(transitionConfig.popenter, transitionConfig.popout); } + } 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.popenter, transitionConfig.popout); + fragmentActivity.overridePendingTransition(transitionConfig.popenterAnimation, transitionConfig.popoutAnimation); } /** @@ -978,6 +1094,9 @@ public void run() { } 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"); @@ -986,68 +1105,50 @@ private boolean checkStateLoss(String logName) { return true; } + @Nullable @Override - public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) { - if (!enter) { - // This is a workaround for the bug where child value disappear when - // the parent is removed (as all children are first removed from the parent) - // See https://code.google.com/p/android/issues/detail?id=55228 - Fragment rootParentFragment = null; - Fragment parentFragment = getParentFragment(); - while (parentFragment != null) { - rootParentFragment = parentFragment; - parentFragment = parentFragment.getParentFragment(); - } - if (rootParentFragment != null && rootParentFragment.isRemoving()) { - Animation doNothingAnim = new AlphaAnimation(1, 1); - int duration = getResources().getInteger(R.integer.qmui_anim_duration); - doNothingAnim.setDuration(duration); - return doNothingAnim; - } - - } - Animation animation = null; - if (enter) { - try { - animation = AnimationUtils.loadAnimation(getContext(), nextAnim); - } catch (Throwable ignored) { - - } - if (animation != null) { - animation.setAnimationListener(new Animation.AnimationListener() { - @Override - public void onAnimationStart(Animation animation) { - onEnterAnimationStart(animation); - } + public final Animation onCreateAnimation(int transit, boolean enter, int nextAnim) { + return null; + } - @Override - public void onAnimationEnd(Animation animation) { - checkAndCallOnEnterAnimationEnd(animation); - } + @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 onAnimationRepeat(Animation animation) { + @Override + public void onAnimationStart(Animator animation) { + checkAndCallOnEnterAnimationStart(animation); + } - } - }); - } else { - onEnterAnimationStart(null); - checkAndCallOnEnterAnimationEnd(null); - } + @Override + public void onAnimationEnd(Animator animation) { + checkAndCallOnEnterAnimationEnd(animation); + } + }); + return animator; } - return animation; + 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 Animation animation) { + private void checkAndCallOnEnterAnimationEnd(@Nullable Animator animation) { mCalled = false; onEnterAnimationEnd(animation); if (!mCalled) { - throw new RuntimeException("QMUIFragment " + this + " did not call through to super.onEnterAnimationEnd(Animation)"); + throw new RuntimeException(getClass().getSimpleName() + " did not call through to super.onEnterAnimationEnd(Animation)"); } } - /** * onCreateView */ @@ -1157,6 +1258,53 @@ 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) { @@ -1238,16 +1386,34 @@ public void runAfterResumed(Runnable runnable) { } } - protected void onEnterAnimationStart(@Nullable Animation animation) { + /** + * 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); } - protected void onEnterAnimationEnd(@Nullable Animation animation) { + /** + * 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<Runnable> list = mDelayRenderRunnableList; mDelayRenderRunnableList = null; @@ -1259,12 +1425,43 @@ protected void onEnterAnimationEnd(@Nullable Animation animation) { } } + public LiveData<Boolean> getIsInEnterAnimationLiveData() { + return isInEnterAnimationLiveData; + } + + protected <T> LiveData<T> enterAnimationAvoidTransform(final LiveData<T> origin){ + return enterAnimationAvoidTransform(origin, isInEnterAnimationLiveData); + } + + protected <T> LiveData<T> enterAnimationAvoidTransform(final LiveData<T> origin, LiveData<Boolean> enterAnimationLiveData){ + final MediatorLiveData<T> result = new MediatorLiveData<T>(); + result.addSource(enterAnimationLiveData, new Observer<Boolean>(){ + + 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<T>() { + @Override + public void onChanged(T t) { + result.setValue(t); + } + }); + } + } + } + }); + return result; + } + @Override public void onDestroy() { super.onDestroy(); - if (mListenerRemover != null) { - mListenerRemover.remove(); - } if (mSwipeBackgroundView != null) { mSwipeBackgroundView.unBind(); mSwipeBackgroundView = null; @@ -1272,23 +1469,11 @@ public void onDestroy() { // help gc, sometimes user may hold fragment instance in somewhere, // then these objects can not be released in time. - mCacheSwipeBackLayout = null; mCacheRootView = null; mDelayRenderRunnableList = null; mCheckPostResumeRunnable = null; } - @Override - public void setUserVisibleHint(boolean isVisibleToUser) { - super.setUserVisibleHint(isVisibleToUser); - notifyFragmentVisibleToUserChanged(isParentVisibleToUser() && isVisibleToUser); - } - - @Override - public boolean isVisibleToUser() { - return getUserVisibleHint() && isParentVisibleToUser(); - } - public boolean onKeyDown(int keyCode, KeyEvent event) { return false; } @@ -1297,50 +1482,15 @@ public boolean onKeyUp(int keyCode, KeyEvent event) { return false; } - /** - * @return true if parentFragments is visible to user - */ - private boolean isParentVisibleToUser() { - Fragment parentFragment = getParentFragment(); - while (parentFragment != null) { - if (!parentFragment.getUserVisibleHint()) { - return false; - } - parentFragment = parentFragment.getParentFragment(); - } - return true; - } - private void notifyFragmentVisibleToUserChanged(boolean isVisibleToUser) { - if (mLazyViewLifecycleOwner != null) { - mLazyViewLifecycleOwner.setViewVisible(isVisibleToUser); - } - if (isAdded()) { - List<Fragment> childFragments = getChildFragmentManager().getFragments(); - for (Fragment fragment : childFragments) { - if (fragment instanceof QMUIFragment) { - ((QMUIFragment) fragment).notifyFragmentVisibleToUserChanged( - isVisibleToUser && fragment.getUserVisibleHint()); - } - } - } + @WindowInsetsCompat.Type.InsetsType + public int getRootViewInsetsType() { + return getParentFragment() == null ? WindowInsetsCompat.Type.ime() : 0; } - public LifecycleOwner getLazyViewLifecycleOwner() { - if (mLazyViewLifecycleOwner == null) { - throw new IllegalStateException("Can't access the QMUIFragment View's LifecycleOwner when " - + "getView() is null i.e., before onViewCreated() or after onDestroyView()"); - } - return mLazyViewLifecycleOwner; - } + @Override + public void refreshFromScheme(@Nullable Bundle bundle) { - /** - * Immersive processing - * - * @return if true, the area under status bar belongs to content; otherwise it belongs to padding - */ - protected boolean translucentFull() { - return false; } /** @@ -1354,6 +1504,15 @@ 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 * @@ -1364,9 +1523,9 @@ protected boolean restoreSubWindowWhenDragBack() { } - public final boolean isStartedByScheme(){ + public final boolean isStartedByScheme() { Bundle arguments = getArguments(); - return arguments != null && arguments.getBoolean(QMUISchemeHandler.ARG_FROM_SCHEME, false); + return arguments != null && arguments.getBoolean(QMUISchemeHandler.ARG_FROM_SCHEME, false); } @@ -1383,16 +1542,22 @@ public static final class TransitionConfig { public final int exit; public final int popenter; public final int popout; - - public TransitionConfig(int enter, int popout) { - this(enter, 0, 0, popout); - } - - public TransitionConfig(int enter, int exit, int popenter, 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; } } } diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/QMUIFragmentActivity.java b/arch/src/main/java/com/qmuiteam/qmui/arch/QMUIFragmentActivity.java index f71d36188..6ef447658 100644 --- a/arch/src/main/java/com/qmuiteam/qmui/arch/QMUIFragmentActivity.java +++ b/arch/src/main/java/com/qmuiteam/qmui/arch/QMUIFragmentActivity.java @@ -19,11 +19,9 @@ import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; -import android.graphics.Rect; import android.os.Bundle; import android.util.Log; import android.view.KeyEvent; -import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; @@ -37,28 +35,25 @@ import com.qmuiteam.qmui.QMUILog; import com.qmuiteam.qmui.arch.annotation.DefaultFirstFragment; -import com.qmuiteam.qmui.arch.first.FirstFragmentFinder; -import com.qmuiteam.qmui.arch.first.FirstFragmentFinders; -import com.qmuiteam.qmui.util.DoNotInterceptKeyboardInset; import com.qmuiteam.qmui.util.QMUIStatusBarHelper; -import com.qmuiteam.qmui.util.QMUIWindowInsetHelper; -import com.qmuiteam.qmui.widget.QMUIWindowInsetLayout; + +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 = "qmui_intent_dst_fragment"; 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 boolean mIsFirstFragmentAdded = false; - - static { - QMUIWindowInsetHelper.addHandleContainer(FragmentContainerView.class); - } + private FragmentAutoInitResult mFragmentAutoInitResult = FragmentAutoInitResult.unHandled; + private boolean isChildHandlePopBackRequested = false; @Override public int getContextViewId() { @@ -70,6 +65,10 @@ public FragmentManager getContainerFragmentManager() { return getSupportFragmentManager(); } + public RootView getRootView() { + return mRootView; + } + @Override @SuppressWarnings("unchecked") protected void onCreate(Bundle savedInstanceState) { @@ -80,48 +79,120 @@ protected void onCreate(Bundle savedInstanceState) { if (savedInstanceState == null) { long start = System.currentTimeMillis(); Intent intent = getIntent(); - Class<? extends QMUIFragment> firstFragmentClass = null; - - // 1. try get first fragment from annotation @FirstFragments. - int dstFragment = intent.getIntExtra(QMUI_INTENT_DST_FRAGMENT, -1); - if (dstFragment != -1) { - FirstFragmentFinder finder = FirstFragmentFinders.getInstance().get(getClass()); - if (finder != null) { - firstFragmentClass = finder.getFragmentClassById(dstFragment); - } - } + // 1. handle muti fragments + mFragmentAutoInitResult = instantiationMutiFragment(intent); - // 2. try get first fragment from fragment class name - if (firstFragmentClass == null) { - String fragmentClassName = intent.getStringExtra(QMUI_INTENT_DST_FRAGMENT_NAME); - if (fragmentClassName != null) { - try { + if (mFragmentAutoInitResult == FragmentAutoInitResult.unHandled) { + try { + Class<? extends QMUIFragment> firstFragmentClass = null; + // 2. try get first fragment from fragment class name + String fragmentClassName = intent.getStringExtra(QMUI_INTENT_DST_FRAGMENT_NAME); + if (fragmentClassName != null && !fragmentClassName.isEmpty()) { firstFragmentClass = (Class<? extends QMUIFragment>) Class.forName(fragmentClassName); - } catch (ClassNotFoundException e) { - QMUILog.d(TAG, "Can not find " + 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; } - } - // 3. try get fragment from annotation @DefaultFirstFragment - if (firstFragmentClass == null) { - firstFragmentClass = getDefaultFirstFragment(); } + Log.i(TAG, "the time it takes to inject first fragment from annotation is " + (System.currentTimeMillis() - start)); + } + } - if (firstFragmentClass != null) { - QMUIFragment firstFragment = instantiationFirstFragment(firstFragmentClass, intent); - if (firstFragment != null) { - getSupportFragmentManager() - .beginTransaction() - .add(getContextViewId(), firstFragment, firstFragment.getClass().getSimpleName()) - .addToBackStack(firstFragment.getClass().getSimpleName()) - .commit(); - mIsFirstFragmentAdded = true; + protected FragmentAutoInitResult instantiationMutiFragment(Intent intent) { + List<Bundle> fragmentBundles = intent.getParcelableArrayListExtra(QMUI_INTENT_FRAGMENT_LIST_ARG); + if (fragmentBundles != null && fragmentBundles.size() > 0) { + List<QMUIFragment> fragments = new ArrayList<>(fragmentBundles.size()); + for (Bundle bundle : fragmentBundles) { + String fragmentClassName = bundle.getString(QMUI_INTENT_DST_FRAGMENT_NAME); + try { + Class<? extends QMUIFragment> cls = (Class<? extends QMUIFragment>) Class.forName(fragmentClassName); + QMUIFragment fragment = instantiationFragment(cls, bundle.getBundle(QMUI_INTENT_FRAGMENT_ARG)); + if (fragment == null) { + return FragmentAutoInitResult.failed; + } + fragments.add(fragment); + } catch (ClassNotFoundException e) { + QMUILog.d(TAG, "Can not find " + fragmentClassName); } } - Log.i(TAG, "the time it takes to inject first fragment from annotation is " + (System.currentTimeMillis() - start)); + if (fragments.size() > 0) { + initMutiFragment(fragments); + return FragmentAutoInitResult.success; + } } + return FragmentAutoInitResult.unHandled; + } + + protected boolean initMutiFragment(QMUIFragment... fragments) { + List<QMUIFragment> list = new ArrayList<>(fragments.length); + Collections.addAll(list, fragments); + return initMutiFragment(list); + } + + protected boolean initMutiFragment(List<QMUIFragment> 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<FragmentTransaction> 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() { @@ -134,12 +205,12 @@ protected void performTranslucent() { * * @return true if first fragment is initialized. */ - protected boolean isFirstFragmentAdded() { - return mIsFirstFragmentAdded; + protected FragmentAutoInitResult isFragmentAutoInitResult() { + return mFragmentAutoInitResult; } - protected void setFirstFragmentAdded(boolean firstFragmentAdded) { - mIsFirstFragmentAdded = firstFragmentAdded; + protected void setFragmentAutoInitResult(FragmentAutoInitResult fragmentAutoInitResult) { + mFragmentAutoInitResult = fragmentAutoInitResult; } protected Class<? extends QMUIFragment> getDefaultFirstFragment() { @@ -156,10 +227,9 @@ protected Class<? extends QMUIFragment> getDefaultFirstFragment() { return null; } - protected QMUIFragment instantiationFirstFragment(Class<? extends QMUIFragment> cls, Intent intent) { + protected QMUIFragment instantiationFragment(Class<? extends QMUIFragment> cls, Bundle args) { try { QMUIFragment fragment = cls.newInstance(); - Bundle args = intent.getBundleExtra(QMUI_INTENT_FRAGMENT_ARG); if (args != null) { fragment.setArguments(args); } @@ -182,6 +252,16 @@ 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); } @@ -219,6 +299,9 @@ private QMUIFragment getCurrentQMUIFragment() { 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; @@ -228,6 +311,7 @@ public int startFragmentAndDestroyCurrent(QMUIFragment fragment, final boolean u 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); @@ -243,6 +327,9 @@ public int startFragmentAndDestroyCurrent(QMUIFragment fragment, final boolean u 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; @@ -252,10 +339,44 @@ public int startFragment(QMUIFragment fragment) { 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<QMUIFragment> 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<FragmentTransaction> 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(); @@ -299,12 +420,6 @@ public static Intent intentOf(@NonNull Context context, @NonNull Class<? extends QMUIFragment> firstFragment, @Nullable Bundle fragmentArgs) { Intent intent = new Intent(context, targetActivity); - FirstFragmentFinder finder = FirstFragmentFinders.getInstance().get(targetActivity); - int dstId = FirstFragmentFinder.NO_ID; - if (finder != null) { - dstId = finder.getIdByFragmentClass(firstFragment); - } - intent.putExtra(QMUI_INTENT_DST_FRAGMENT, dstId); intent.putExtra(QMUI_INTENT_DST_FRAGMENT_NAME, firstFragment.getName()); if (fragmentArgs != null) { intent.putExtra(QMUI_INTENT_FRAGMENT_ARG, fragmentArgs); @@ -324,30 +439,28 @@ public static Intent intentOf(@NonNull Context context, return intent; } - public static abstract class RootView extends QMUIWindowInsetLayout { + public static abstract class RootView extends FrameLayout { public RootView(Context context, int fragmentContainerId) { super(context); + setId(R.id.qmui_activity_root_id); } - @Override - public boolean applySystemWindowInsets21(Object insets) { - super.applySystemWindowInsets21(insets); - return true; - } + public abstract FragmentContainerView getFragmentContainerView(); + } - @Override - public boolean applySystemWindowInsets19(Rect insets) { - super.applySystemWindowInsets19(insets); - return true; + @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. } - - public abstract FragmentContainerView getFragmentContainerView(); } @SuppressLint("ViewConstructor") - @DoNotInterceptKeyboardInset public static class DefaultRootView extends RootView { private FragmentContainerView mFragmentContainerView; @@ -358,14 +471,6 @@ public DefaultRootView(Context context, int fragmentContainerId) { addView(mFragmentContainerView, new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - mFragmentContainerView.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) { - for (int i = 0; i < getChildCount(); i++) { - SwipeBackLayout.updateLayoutInSwipeBack(getChildAt(i)); - } - } - }); } @Override @@ -373,4 +478,6 @@ public FragmentContainerView getFragmentContainerView() { return mFragmentContainerView; } } + + public enum FragmentAutoInitResult {success, failed, unHandled} } \ No newline at end of file diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/QMUIFragmentContainerProvider.java b/arch/src/main/java/com/qmuiteam/qmui/arch/QMUIFragmentContainerProvider.java index 9bc90ff3c..5a9d3b7f7 100644 --- a/arch/src/main/java/com/qmuiteam/qmui/arch/QMUIFragmentContainerProvider.java +++ b/arch/src/main/java/com/qmuiteam/qmui/arch/QMUIFragmentContainerProvider.java @@ -30,4 +30,8 @@ public interface QMUIFragmentContainerProvider { FragmentContainerView getFragmentContainerView(); ViewModelStoreOwner getContainerViewModelStoreOwner(); + + void requestForHandlePopBack(boolean toHandle); + + boolean isChildHandlePopBackRequested(); } diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/QMUIFragmentLazyLifecycleOwner.java b/arch/src/main/java/com/qmuiteam/qmui/arch/QMUIFragmentLazyLifecycleOwner.java deleted file mode 100644 index bbc9d0191..000000000 --- a/arch/src/main/java/com/qmuiteam/qmui/arch/QMUIFragmentLazyLifecycleOwner.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.NonNull; -import androidx.lifecycle.Lifecycle; -import androidx.lifecycle.LifecycleObserver; -import androidx.lifecycle.LifecycleOwner; -import androidx.lifecycle.LifecycleRegistry; -import androidx.lifecycle.OnLifecycleEvent; - -public class QMUIFragmentLazyLifecycleOwner implements LifecycleOwner, LifecycleObserver { - - private LifecycleRegistry mLifecycleRegistry = null; - private boolean mIsViewVisible = true; - private Lifecycle.State mViewState = Lifecycle.State.INITIALIZED; - private Callback mCallback; - - public QMUIFragmentLazyLifecycleOwner(@NonNull Callback callback){ - mCallback = callback; - } - - /** - * Initializes the underlying Lifecycle if it hasn't already been created. - */ - void initialize() { - if (mLifecycleRegistry == null) { - mLifecycleRegistry = new LifecycleRegistry(this); - } - } - - void setViewVisible(boolean viewVisible) { - if(mViewState.compareTo(Lifecycle.State.CREATED) < 0 || !isInitialized()){ - // not trust it before onCreate - return; - } - mIsViewVisible = viewVisible; - if (viewVisible) { - mLifecycleRegistry.markState(mViewState); - } else { - if (mViewState.compareTo(Lifecycle.State.CREATED) > 0) { - mLifecycleRegistry.markState(Lifecycle.State.CREATED); - } else { - mLifecycleRegistry.markState(mViewState); - } - } - } - - /** - * @return True if the Lifecycle has been initialized. - */ - boolean isInitialized() { - return mLifecycleRegistry != null; - } - - @NonNull - @Override - public Lifecycle getLifecycle() { - initialize(); - return mLifecycleRegistry; - } - - private void handleLifecycleEvent(@NonNull Lifecycle.Event event) { - initialize(); - mLifecycleRegistry.handleLifecycleEvent(event); - } - - - @OnLifecycleEvent(Lifecycle.Event.ON_CREATE) - void onCreate(LifecycleOwner owner) { - mIsViewVisible = mCallback.isVisibleToUser(); - mViewState = Lifecycle.State.CREATED; - handleLifecycleEvent(Lifecycle.Event.ON_CREATE); - } - - @OnLifecycleEvent(Lifecycle.Event.ON_START) - void onStart(LifecycleOwner owner) { - mViewState = Lifecycle.State.STARTED; - if (mIsViewVisible) { - handleLifecycleEvent(Lifecycle.Event.ON_START); - } - } - - @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) - void onResume(LifecycleOwner owner) { - mViewState = Lifecycle.State.RESUMED; - if (mIsViewVisible && mLifecycleRegistry.getCurrentState() == Lifecycle.State.STARTED) { - handleLifecycleEvent(Lifecycle.Event.ON_RESUME); - } - } - - @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) - void onPause(LifecycleOwner owner) { - mViewState = Lifecycle.State.STARTED; - if (mLifecycleRegistry.getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) { - handleLifecycleEvent(Lifecycle.Event.ON_PAUSE); - } - } - - @OnLifecycleEvent(Lifecycle.Event.ON_STOP) - void onStop(LifecycleOwner owner) { - mViewState = Lifecycle.State.CREATED; - if (mLifecycleRegistry.getCurrentState().isAtLeast(Lifecycle.State.STARTED)) { - handleLifecycleEvent(Lifecycle.Event.ON_STOP); - } - } - - @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) - void onDestroy(LifecycleOwner owner) { - mViewState = Lifecycle.State.DESTROYED; - handleLifecycleEvent(Lifecycle.Event.ON_DESTROY); - } - - interface Callback { - boolean isVisibleToUser(); - } -} \ No newline at end of file diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/QMUIFragmentPagerAdapter.java b/arch/src/main/java/com/qmuiteam/qmui/arch/QMUIFragmentPagerAdapter.java index bde97c445..d4eea7a25 100644 --- a/arch/src/main/java/com/qmuiteam/qmui/arch/QMUIFragmentPagerAdapter.java +++ b/arch/src/main/java/com/qmuiteam/qmui/arch/QMUIFragmentPagerAdapter.java @@ -20,12 +20,13 @@ import android.view.View; import android.view.ViewGroup; -import com.qmuiteam.qmui.widget.QMUIPagerAdapter; - 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 { @@ -78,17 +79,21 @@ protected void populate(@NonNull ViewGroup container, @NonNull Object item, int } if (fragment != mCurrentPrimaryItem) { fragment.setMenuVisibility(false); - fragment.setUserVisibleHint(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) object); + mCurrentTransaction.detach(fragment); + if (fragment == mCurrentPrimaryItem) { + mCurrentPrimaryItem = null; + } } @Override @@ -113,10 +118,16 @@ public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull if (fragment != mCurrentPrimaryItem) { if (mCurrentPrimaryItem != null) { mCurrentPrimaryItem.setMenuVisibility(false); - mCurrentPrimaryItem.setUserVisibleHint(false); + if (mCurrentTransaction == null) { + mCurrentTransaction = mFragmentManager.beginTransaction(); + } + mCurrentTransaction.setMaxLifecycle(mCurrentPrimaryItem, Lifecycle.State.STARTED); } fragment.setMenuVisibility(true); - fragment.setUserVisibleHint(true); + if (mCurrentTransaction == null) { + mCurrentTransaction = mFragmentManager.beginTransaction(); + } + mCurrentTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED); mCurrentPrimaryItem = fragment; } } diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/QMUINavFragment.java b/arch/src/main/java/com/qmuiteam/qmui/arch/QMUINavFragment.java index 6dcfe320d..b3181ba9f 100644 --- a/arch/src/main/java/com/qmuiteam/qmui/arch/QMUINavFragment.java +++ b/arch/src/main/java/com/qmuiteam/qmui/arch/QMUINavFragment.java @@ -8,6 +8,7 @@ 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; @@ -18,6 +19,7 @@ public class QMUINavFragment extends QMUIFragment implements QMUIFragmentContain private static final String QMUI_ARGUMENT_FRAGMENT_ARG = "qmui_argument_fragment_arg"; private FragmentContainerView mContainerView; private boolean mIsFirstFragmentAdded = false; + private boolean isChildHandlePopBackRequested = false; public static QMUINavFragment getDefaultInstance(Class<? extends QMUIFragment> firstFragmentCls, @Nullable Bundle firstFragmentArgument){ @@ -44,18 +46,6 @@ static Bundle initArguments(String firstFragmentClsName, @Nullable Bundle firstF return arg; } - @Override - public void onAttach(@NonNull Context context) { - super.onAttach(context); - getChildFragmentManager().addOnBackStackChangedListener(new FragmentManager.OnBackStackChangedListener() { - @Override - public void onBackStackChanged() { - getParentFragmentManager().beginTransaction() - .setPrimaryNavigationFragment(getChildFragmentManager().getBackStackEntryCount() > 1 ? QMUINavFragment.this : null) - .commit(); - } - }); - } @Override public void onCreate(@Nullable Bundle savedInstanceState) { @@ -112,20 +102,22 @@ private QMUIFragment instantiationFirstFragment(String clsName, Bundle arguments @Override protected View onCreateView() { - setContainerView(new FragmentContainerView(getContext())); - return mContainerView; + FragmentContainerView rootView = new FragmentContainerView(getContext()); + rootView.setId(getContextViewId()); + return rootView; } @Override - protected void onViewCreated(@NonNull View rootView) { + 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 #setContainerView() in onCreateView()"); + throw new RuntimeException("must call #configFragmentContainerView() in onCreateView()"); } } - protected void setContainerView(FragmentContainerView fragmentContainerView){ - mContainerView = fragmentContainerView; - mContainerView.setId(getContextViewId()); + protected void configFragmentContainerView(FragmentContainerView fragmentContainerView){ + fragmentContainerView.setId(getContextViewId()); } @Override @@ -139,6 +131,56 @@ 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(); diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/QMUISwipeBackActivityManager.java b/arch/src/main/java/com/qmuiteam/qmui/arch/QMUISwipeBackActivityManager.java index 170406e4f..071de58af 100644 --- a/arch/src/main/java/com/qmuiteam/qmui/arch/QMUISwipeBackActivityManager.java +++ b/arch/src/main/java/com/qmuiteam/qmui/arch/QMUISwipeBackActivityManager.java @@ -53,12 +53,18 @@ public static void init(@NonNull Application application) { @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 @@ -91,6 +97,18 @@ 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/ @@ -120,7 +138,11 @@ public Activity getPenultimateActivity(Activity currentActivity) { return activity; } - public boolean canSwipeBack() { - return mActivityStack.size() > 1; + public boolean canSwipeBack(Activity currentActivity) { + if(currentActivity == null){ + return false; + } + Activity prevActivity = getPenultimateActivity(currentActivity); + return prevActivity != null && !prevActivity.isDestroyed() && !prevActivity.isFinishing(); } } diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/SwipeBackLayout.java b/arch/src/main/java/com/qmuiteam/qmui/arch/SwipeBackLayout.java index 3ae706ca8..c7d35dcde 100644 --- a/arch/src/main/java/com/qmuiteam/qmui/arch/SwipeBackLayout.java +++ b/arch/src/main/java/com/qmuiteam/qmui/arch/SwipeBackLayout.java @@ -19,8 +19,8 @@ import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; -import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.os.SystemClock; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.MotionEvent; @@ -28,19 +28,22 @@ 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.widget.QMUIWindowInsetLayout; +import com.qmuiteam.qmui.util.QMUIWindowInsetHelper; import java.util.ArrayList; import java.util.List; -import androidx.annotation.NonNull; -import androidx.core.view.ViewCompat; - import static com.qmuiteam.qmui.QMUIInterpolatorStaticHolder.QUNITIC_INTERPOLATOR; /** @@ -50,7 +53,7 @@ */ -public class SwipeBackLayout extends QMUIWindowInsetLayout { +public class SwipeBackLayout extends FrameLayout { private static final int MIN_FLING_VELOCITY = 400; // dips per second private static final int DEFAULT_SCRIM_COLOR = 0x99000000; @@ -83,9 +86,9 @@ public class SwipeBackLayout extends QMUIWindowInsetLayout { private float mScrollThreshold = DEFAULT_SCROLL_THRESHOLD; private View mContentView; - private float mScrollPercent; private List<SwipeListener> mListeners; private Callback mCallback; + private OnInsetsHandler mOnInsetsHandler; private Drawable mShadowLeft; private Drawable mShadowRight; @@ -113,6 +116,9 @@ public class SwipeBackLayout extends QMUIWindowInsetLayout { private boolean mIsScrollOverValid = true; private boolean mEnableSwipeBack = true; + private int mRequestLayoutCount = 0; + private long mRequestLayoutCheckStartTime = -1; + public SwipeBackLayout(Context context) { this(context, null); @@ -148,6 +154,19 @@ public SwipeBackLayout(Context context, AttributeSet attrs, int defStyle) { 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) { @@ -236,35 +255,8 @@ public void clearSwipeListeners() { mListeners = null; } - 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 void setOnInsetsHandler(OnInsetsHandler insetsHandler) { + mOnInsetsHandler = insetsHandler; } /** @@ -335,6 +327,7 @@ private int selectDragDirection(float x, float y) { mInitialMotionX = mLastMotionX = x; mInitialMotionY = mLastMotionY = y; onSwipeBackBegin(); + requestParentDisallowInterceptTouchEvent(true); setDragState(STATE_DRAGGING); } return mCurrentDragDirection; @@ -349,6 +342,32 @@ private float getTouchMoveDelta(float x, float y) { } } + @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){ @@ -375,6 +394,7 @@ public boolean onInterceptTouchEvent(MotionEvent ev) { mInitialMotionY = mLastMotionY = y; if (mDragState == STATE_SETTLING) { if (isTouchInContentView(x, y)) { + requestParentDisallowInterceptTouchEvent(true); setDragState(STATE_DRAGGING); } } @@ -390,6 +410,7 @@ public boolean onInterceptTouchEvent(MotionEvent ev) { onScroll(); } else { if (isTouchInContentView(x, y)) { + requestParentDisallowInterceptTouchEvent(true); setDragState(STATE_DRAGGING); } } @@ -433,6 +454,7 @@ public boolean onTouchEvent(MotionEvent ev) { mInitialMotionY = mLastMotionY = y; if (mDragState == STATE_SETTLING) { if (isTouchInContentView(x, y)) { + requestParentDisallowInterceptTouchEvent(true); setDragState(STATE_DRAGGING); } } @@ -448,6 +470,7 @@ public boolean onTouchEvent(MotionEvent ev) { onScroll(); } else { if (isTouchInContentView(x, y)) { + requestParentDisallowInterceptTouchEvent(true); setDragState(STATE_DRAGGING); } } @@ -477,6 +500,13 @@ public boolean onTouchEvent(MotionEvent ev) { 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); @@ -659,18 +689,6 @@ protected void onLayout(boolean changed, int left, int top, int right, int botto } } - @Override - public boolean applySystemWindowInsets19(Rect insets) { - super.applySystemWindowInsets19(insets); - return true; - } - - @Override - public boolean applySystemWindowInsets21(Object insets) { - super.applySystemWindowInsets21(insets); - return true; - } - @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { final boolean drawContent = child == mContentView; @@ -748,19 +766,19 @@ private void onSwipeBackBegin() { } private void onScroll() { - mScrollPercent = mViewMoveAction.getCurrentPercent(this, mContentView, mCurrentDragDirection); + float scrollPercent = mViewMoveAction.getCurrentPercent(this, mContentView, mCurrentDragDirection); mScrimOpacity = 1 - mViewMoveAction.getCurrentPercent(this, mContentView, mCurrentDragDirection); - if (mScrollPercent < mScrollThreshold && !mIsScrollOverValid) { + if (scrollPercent < mScrollThreshold && !mIsScrollOverValid) { mIsScrollOverValid = true; } if (mDragState == STATE_DRAGGING && mIsScrollOverValid && - mScrollPercent >= mScrollThreshold) { + scrollPercent >= mScrollThreshold) { mIsScrollOverValid = false; onScrollOverThreshold(); } if (mListeners != null && !mListeners.isEmpty()) { for (SwipeListener listener : mListeners) { - listener.onScroll(mCurrentDragDirection, mViewMoveAction.getEdge(mCurrentDragDirection), mScrollPercent); + listener.onScroll(mCurrentDragDirection, mViewMoveAction.getEdge(mCurrentDragDirection), scrollPercent); } } invalidate(); @@ -783,6 +801,12 @@ private void onViewDragStateChanged(int dragState) { } } + 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( @@ -804,39 +828,71 @@ public static SwipeBackLayout wrap(Context context, int childRes, ViewMoveAction return wrapper; } - public static void offsetInSwipeBack(View view, int edgeFlag, int targetOffset) { - Object offsetHelperObj = view.getTag(R.id.qmui_arch_swipe_offset_helper); - QMUIViewOffsetHelper offsetHelper; - if (!(offsetHelperObj instanceof QMUIViewOffsetHelper)) { - offsetHelper = new QMUIViewOffsetHelper(view); - view.setTag(R.id.qmui_arch_swipe_offset_helper, offsetHelper); - } else { - offsetHelper = (QMUIViewOffsetHelper) offsetHelperObj; - } + public static void translateInSwipeBack(View view, int edgeFlag, int targetOffset){ if (edgeFlag == EDGE_BOTTOM) { - offsetHelper.setTopAndBottomOffset(targetOffset); - offsetHelper.setLeftAndRightOffset(0); + view.setTranslationY(targetOffset); + view.setTranslationX(0); } else if (edgeFlag == EDGE_RIGHT) { - offsetHelper.setTopAndBottomOffset(0); - offsetHelper.setLeftAndRightOffset(targetOffset); - } else { - offsetHelper.setTopAndBottomOffset(0); - offsetHelper.setLeftAndRightOffset(-targetOffset); + 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 static void updateLayoutInSwipeBack(View view) { - if (view.getTag(R.id.qmui_arch_swipe_layout_in_back) == QMUIFragment.SWIPE_BACK_VIEW) { - Object offsetHelperObj = view.getTag(R.id.qmui_arch_swipe_offset_helper); - if (offsetHelperObj instanceof QMUIViewOffsetHelper) { - ((QMUIViewOffsetHelper) offsetHelperObj).onViewLayout(); + 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 { @@ -861,6 +917,42 @@ 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){ diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/SwipeBackgroundView.java b/arch/src/main/java/com/qmuiteam/qmui/arch/SwipeBackgroundView.java index d0dde360f..f609ff0fe 100644 --- a/arch/src/main/java/com/qmuiteam/qmui/arch/SwipeBackgroundView.java +++ b/arch/src/main/java/com/qmuiteam/qmui/arch/SwipeBackgroundView.java @@ -36,17 +36,16 @@ import java.util.ArrayList; import java.util.List; -class SwipeBackgroundView extends View { +public class SwipeBackgroundView extends View { private ArrayList<ViewInfo> mViewWeakReference; private boolean mDoRotate = false; - public SwipeBackgroundView(Context context) { + public SwipeBackgroundView(Context context, boolean forceDisableHardwareAccelerated) { super(context); - } - - public SwipeBackgroundView(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); + if(forceDisableHardwareAccelerated){ + setLayerType(LAYER_TYPE_SOFTWARE, null); + } } public void bind(Activity activity, Activity swipeActivity, boolean restoreForSubWindow) { @@ -188,7 +187,9 @@ void draw(Canvas canvas) { if (isMain || lp == null) { view.draw(canvas); } else { - canvas.drawColor(QMUIColorHelper.setColorAlpha(Color.BLACK, lp.dimAmount)); + 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); diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/Utils.java b/arch/src/main/java/com/qmuiteam/qmui/arch/Utils.java index 4a8fb4407..b8c206878 100644 --- a/arch/src/main/java/com/qmuiteam/qmui/arch/Utils.java +++ b/arch/src/main/java/com/qmuiteam/qmui/arch/Utils.java @@ -17,22 +17,20 @@ package com.qmuiteam.qmui.arch; import android.annotation.SuppressLint; -import android.annotation.TargetApi; import android.app.Activity; import android.app.ActivityOptions; -import android.os.Build; 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; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentTransaction; - /** * Created by Chaojun Wang on 6/9/14. */ @@ -74,42 +72,8 @@ public static void convertActivityFromTranslucent(Activity activity) { * with the {@link android.R.attr#windowIsFloating} attribute. */ public static void convertActivityToTranslucent(Activity activity) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - convertActivityToTranslucentAfterL(activity); - } else { - convertActivityToTranslucentBeforeL(activity); - } - } - - /** - * Calling the convertToTranslucent method on platforms before Android 5.0 - */ - private static void convertActivityToTranslucentBeforeL(Activity activity) { try { - Class<?>[] classes = Activity.class.getDeclaredClasses(); - Class<?> translucentConversionListenerClazz = null; - for (Class clazz : classes) { - if (clazz.getSimpleName().contains("TranslucentConversionListener")) { - translucentConversionListenerClazz = clazz; - } - } - @SuppressLint("PrivateApi") Method method = Activity.class.getDeclaredMethod("convertToTranslucent", - translucentConversionListenerClazz); - method.setAccessible(true); - method.invoke(activity, new Object[]{ - null - }); - } catch (Throwable ignore) { - } - } - - /** - * Calling the convertToTranslucent method on platforms after Android 5.0 - */ - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - private static void convertActivityToTranslucentAfterL(Activity activity) { - try { - @SuppressLint("PrivateApi") Method getActivityOptions = Activity.class.getDeclaredMethod("getActivityOptions"); + @SuppressLint({"PrivateApi", "DiscouragedPrivateApi"}) Method getActivityOptions = Activity.class.getDeclaredMethod("getActivityOptions"); getActivityOptions.setAccessible(true); Object options = getActivityOptions.invoke(activity); @@ -128,6 +92,7 @@ private static void convertActivityToTranslucentAfterL(Activity activity) { } } + public static void assertInMainThread() { if (Looper.myLooper() != Looper.getMainLooper()) { StackTraceElement[] elements = Thread.currentThread().getStackTrace(); diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/effect/QMUIFragmentEffectRegistry.java b/arch/src/main/java/com/qmuiteam/qmui/arch/effect/QMUIFragmentEffectRegistry.java index 5f0a54aab..77613e869 100644 --- a/arch/src/main/java/com/qmuiteam/qmui/arch/effect/QMUIFragmentEffectRegistry.java +++ b/arch/src/main/java/com/qmuiteam/qmui/arch/effect/QMUIFragmentEffectRegistry.java @@ -17,6 +17,8 @@ package com.qmuiteam.qmui.arch.effect; +import android.util.ArraySet; + import androidx.annotation.MainThread; import androidx.annotation.NonNull; import androidx.lifecycle.Lifecycle; @@ -24,27 +26,57 @@ import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.ViewModel; +import com.qmuiteam.qmui.QMUIConfig; import com.qmuiteam.qmui.QMUILog; -import com.qmuiteam.qmui.arch.BuildConfig; 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<T extends Effect> implements QMUIFragmentEffectRegistration{ + final LifecycleOwner lifecycleOwner; + final QMUIFragmentEffectHandler<T> effectHandler; + private QMUIFragmentEffectRegistration registration; + + + public PendingRegister(LifecycleOwner lifecycleOwner, QMUIFragmentEffectHandler<T> 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<Integer, EffectHandlerWrapper> mKeyToHandler = new HashMap<>(); - private final Map<Class<? extends Effect>, List<Integer>> mEffectTypeToRcs = new HashMap<>(); + private final transient Map<Integer, EffectHandlerWrapper<?>> mKeyToHandler = new HashMap<>(); + private transient int mNotifyEffectRunning = 0; + private final transient Set<Integer> mPendingRemoveKeys = new HashSet<>(); + private final transient List<PendingRegister<?>> mPendingRegister = new ArrayList<>(); /** @@ -61,26 +93,22 @@ public class QMUIFragmentEffectRegistry extends ViewModel { public <T extends Effect> QMUIFragmentEffectRegistration register( @NonNull final LifecycleOwner lifecycleOwner, @NonNull final QMUIFragmentEffectHandler<T> effectHandler) { + if(mNotifyEffectRunning > 0){ + PendingRegister<T> pendingRegister = new PendingRegister<>(lifecycleOwner, effectHandler); + mPendingRegister.add(pendingRegister); + return pendingRegister; + } final int rc = mNextRc.getAndIncrement(); Lifecycle lifecycle = lifecycleOwner.getLifecycle(); mKeyToHandler.put(rc, new EffectHandlerWrapper<T>(effectHandler, lifecycle)); - lifecycle.addObserver(new LifecycleEventObserver() { - @Override - public void onStateChanged(@NonNull LifecycleOwner lifecycleOwner, - @NonNull Lifecycle.Event event) { - if (Lifecycle.Event.ON_DESTROY.equals(event)) { - unregister(rc); - } + lifecycle.addObserver((LifecycleEventObserver) (lifecycleOwner1, event) -> { + if (Lifecycle.Event.ON_DESTROY.equals(event)) { + unregister(rc); } }); - return new QMUIFragmentEffectRegistration() { - @Override - public void unregister() { - QMUIFragmentEffectRegistry.this.unregister(rc); - } - }; + return () -> QMUIFragmentEffectRegistry.this.unregister(rc); } /** @@ -91,7 +119,15 @@ public void unregister() { */ @MainThread final void unregister(int key) { - EffectHandlerWrapper effectHandlerWrapper = mKeyToHandler.remove(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(); } @@ -105,12 +141,28 @@ final void unregister(int key) { * @param effect */ public <T extends Effect> void notifyEffect(T effect) { + mNotifyEffectRunning++; for (Integer key : mKeyToHandler.keySet()) { - EffectHandlerWrapper wrapper = mKeyToHandler.get(key); + 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<T extends Effect> implements LifecycleEventObserver { @@ -148,7 +200,7 @@ private Class<? extends Effect> getHandlerEffectType(QMUIFragmentEffectHandler h } if (effectClz == null) { - if (BuildConfig.DEBUG) { + 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"); @@ -160,7 +212,7 @@ private Class<? extends Effect> getHandlerEffectType(QMUIFragmentEffectHandler h @SuppressWarnings("unchecked") boolean shouldHandleEffect(Effect effect) { - return effect.getClass() == mEffectType && mHandler.shouldHandleEffect((T) effect); + return mEffectType != null && mEffectType.isAssignableFrom(effect.getClass()) && mHandler.shouldHandleEffect((T) effect); } @MainThread diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/first/FirstFragmentFinder.java b/arch/src/main/java/com/qmuiteam/qmui/arch/first/FirstFragmentFinder.java deleted file mode 100644 index 6adefcc16..000000000 --- a/arch/src/main/java/com/qmuiteam/qmui/arch/first/FirstFragmentFinder.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.first; - -import com.qmuiteam.qmui.arch.QMUIFragment; - -public interface FirstFragmentFinder { - int NO_ID = -1; - Class<? extends QMUIFragment> getFragmentClassById(int id); - int getIdByFragmentClass(Class<? extends QMUIFragment> clazz); -} diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/first/FirstFragmentFinders.java b/arch/src/main/java/com/qmuiteam/qmui/arch/first/FirstFragmentFinders.java deleted file mode 100644 index c94c4b0ac..000000000 --- a/arch/src/main/java/com/qmuiteam/qmui/arch/first/FirstFragmentFinders.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.first; - -import android.util.Log; - -import com.qmuiteam.qmui.arch.QMUIFragment; -import com.qmuiteam.qmui.arch.QMUIFragmentActivity; - -import java.util.HashMap; - -import androidx.annotation.MainThread; - -public class FirstFragmentFinders { - private static FirstFragmentFinders instance; - private static boolean debug = false; - private static final String TAG = "FirstFragmentFinders"; - private static final FirstFragmentFinder EMPTY_FINDER = new FirstFragmentFinder() { - @Override - public Class<? extends QMUIFragment> getFragmentClassById(int id) { - return null; - } - - @Override - public int getIdByFragmentClass(Class<? extends QMUIFragment> clazz) { - return FirstFragmentFinder.NO_ID; - } - }; - - public static void setDebug(boolean debug) { - FirstFragmentFinders.debug = debug; - } - - @MainThread - public static FirstFragmentFinders getInstance() { - if (instance == null) { - instance = new FirstFragmentFinders(); - } - return instance; - } - - private HashMap<Class<?>, FirstFragmentFinder> mCache = new HashMap<>(); - - private FirstFragmentFinders() { - - } - - public FirstFragmentFinder get(Class<? extends QMUIFragmentActivity> cls) { - FirstFragmentFinder finder = mCache.get(cls); - if (finder != null) { - return finder; - } - - ClassLoader classLoader = cls.getClassLoader(); - if (classLoader == null) { - return null; - } - String clsName = cls.getName(); - - try { - Class<?> finderClass = classLoader.loadClass(clsName + "_FragmentFinder"); - if (FirstFragmentFinder.class.isAssignableFrom(finderClass)) { - finder = (FirstFragmentFinder) finderClass.newInstance(); - } - } catch (ClassNotFoundException e) { - Class<?> superClass = cls.getSuperclass(); - if (superClass != null && QMUIFragmentActivity.class.isAssignableFrom(superClass)) { - if(debug){ - Log.d(TAG, "Not found. Trying superclass" + superClass.getName()); - } - finder = get((Class<? extends QMUIFragmentActivity>) superClass); - } - } catch (IllegalAccessException e) { - if (debug) { - Log.d(TAG, "Access exception."); - e.printStackTrace(); - } - } catch (InstantiationException e) { - if (debug) { - Log.d(TAG, "Instantiation exception."); - e.printStackTrace(); - } - } - - if (finder == null) { - finder = EMPTY_FINDER; - } - mCache.put(cls, finder); - return finder; - } -} diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/ActivitySchemeItem.java b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/ActivitySchemeItem.java deleted file mode 100644 index cd43b38a5..000000000 --- a/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/ActivitySchemeItem.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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 androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.qmuiteam.qmui.QMUILog; - -import java.util.HashMap; -import java.util.Map; - -class ActivitySchemeItem extends SchemeItem { - private static HashMap<Class<? extends QMUISchemeIntentFactory>, QMUISchemeIntentFactory> sFactories; - - @NonNull - private final Class<? extends Activity> mActivityClass; - @Nullable - private final Class<? extends QMUISchemeIntentFactory> mIntentFactoryCls; - - public ActivitySchemeItem(@NonNull Class<? extends Activity> activityClass, - @Nullable Class<? extends QMUISchemeIntentFactory> intentFactoryCls, - @Nullable ArrayMap<String, String> required, - @Nullable String[] keysForInt, - @Nullable String[] keysForBool, - @Nullable String[] keysForLong, - @Nullable String[] keysForFloat, - @Nullable String[] keysForDouble) { - super(required, keysForInt, keysForBool, keysForLong, keysForFloat, keysForDouble); - mActivityClass = activityClass; - mIntentFactoryCls = intentFactoryCls; - } - - @Override - public boolean handle(@NonNull QMUISchemeHandler handler, - @NonNull Activity activity, - @Nullable Map<String, SchemeValue> scheme) { - if (sFactories == null) { - sFactories = new HashMap<>(); - } - Class<? extends QMUISchemeIntentFactory> factoryCls = mIntentFactoryCls; - if(factoryCls == null){ - factoryCls = handler.getDefaultIntentFactory(); - } - - QMUISchemeIntentFactory factory = sFactories.get(factoryCls); - if (factory == null) { - try { - factory = factoryCls.newInstance(); - sFactories.put(factoryCls, factory); - } catch (Exception e) { - QMUILog.printErrStackTrace(QMUISchemeHandler.TAG, e, - "error to instance QMUISchemeIntentFactory: %d", factoryCls.getSimpleName()); - } - } - - if (factory != null) { - if (factory.shouldBlockJump(activity, mActivityClass, scheme)) { - return true; - } - - Intent intent = factory.factory(activity, mActivityClass, scheme); - activity.startActivity(intent); - return true; - } - return false; - } -} diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/ActivitySchemeItem.kt b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/ActivitySchemeItem.kt new file mode 100644 index 000000000..588f5b4eb --- /dev/null +++ b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/ActivitySchemeItem.kt @@ -0,0 +1,88 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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<Class<out QMUISchemeIntentFactory>, QMUISchemeIntentFactory>() } + +internal class ActivitySchemeItem( + private val activityClass: Class<out Activity>, + useRefreshIfMatchedCurrent: Boolean, + private val intentFactoryCls: Class<out QMUISchemeIntentFactory>?, + required: ArrayMap<String, String?>?, + keysForInt: Array<String>?, + keysForBool: Array<String>?, + keysForLong: Array<String>?, + keysForFloat: Array<String>?, + keysForDouble: Array<String>?, + defaultParams: Array<String>?, + schemeMatcherCls: Class<out QMUISchemeMatcher>?, + schemeValueConverterCls: Class<out QMUISchemeValueConverter>? +) : 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 + } +} \ No newline at end of file diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/FragmentSchemeItem.java b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/FragmentSchemeItem.java deleted file mode 100644 index d8f81cfce..000000000 --- a/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/FragmentSchemeItem.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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 androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.qmuiteam.qmui.QMUILog; -import com.qmuiteam.qmui.arch.QMUIFragment; -import com.qmuiteam.qmui.arch.QMUIFragmentActivity; - -import java.util.HashMap; -import java.util.Map; - -class FragmentSchemeItem extends SchemeItem { - private static HashMap<Class<? extends QMUISchemeFragmentFactory>, QMUISchemeFragmentFactory> sFactories; - private final Class<? extends QMUIFragment> mFragmentCls; - @NonNull - private final Class<? extends QMUIFragmentActivity>[] mActivityClsList; - private final boolean mForceNewActivity; - private final String mForceNewActivityKey; - @Nullable - private final Class<? extends QMUISchemeFragmentFactory> mFragmentFactoryCls; - - public FragmentSchemeItem(@NonNull Class<? extends QMUIFragment> fragmentCls, - @NonNull Class<? extends QMUIFragmentActivity>[] activityClsList, - @Nullable Class<? extends QMUISchemeFragmentFactory> fragmentFactoryCls, - boolean forceNewActivity, - @Nullable String forceNewActivityKey, - @Nullable ArrayMap<String, String> required, - @Nullable String[] keysForInt, - @Nullable String[] keysForBool, - @Nullable String[] keysForLong, - @Nullable String[] keysForFloat, - @Nullable String[] keysForDouble) { - super(required, keysForInt, keysForBool, keysForLong, keysForFloat, keysForDouble); - mFragmentCls = fragmentCls; - mActivityClsList = activityClsList; - mForceNewActivity = forceNewActivity; - mForceNewActivityKey = forceNewActivityKey; - mFragmentFactoryCls = fragmentFactoryCls; - } - - @Override - public boolean handle(@NonNull QMUISchemeHandler handler, - @NonNull Activity activity, - @Nullable Map<String, SchemeValue> scheme) { - if (mActivityClsList.length == 0) { - QMUILog.d(QMUISchemeHandler.TAG, "Can not start a new fragment because the host is't provided"); - return false; - } - if (sFactories == null) { - sFactories = new HashMap<>(); - } - - Class<? extends QMUISchemeFragmentFactory> factoryCls = mFragmentFactoryCls; - if (factoryCls == null) { - factoryCls = handler.getDefaultFragmentFactory(); - } - - QMUISchemeFragmentFactory factory = sFactories.get(factoryCls); - if (factory == null) { - try { - factory = factoryCls.newInstance(); - sFactories.put(factoryCls, factory); - } catch (Exception e) { - QMUILog.printErrStackTrace(QMUISchemeHandler.TAG, e, - "error to instance QMUISchemeFragmentFactory: %d", factoryCls.getSimpleName()); - } - } - if (factory == null) { - return false; - } - - if (factory.shouldBlockJump(activity, mFragmentCls, scheme)) { - return true; - } - - if (!isCurrentActivityCanStartFragment(activity) || isForceNewActivity(scheme)) { - Intent intent = factory.factory(activity, mActivityClsList, mFragmentCls, scheme); - if (intent != null) { - activity.startActivity(intent); - return true; - } - return false; - } - - QMUIFragmentActivity fragmentActivity = (QMUIFragmentActivity) activity; - QMUIFragment fragment = factory.factory(mFragmentCls, scheme); - if (fragment != null) { - int commitId = fragmentActivity.startFragment(fragment); - if (commitId == -1) { - QMUILog.d(QMUISchemeHandler.TAG, "start fragment failed."); - return false; - } - return true; - } - return false; - } - - private boolean isCurrentActivityCanStartFragment(Activity activity) { - if (!(activity instanceof QMUIFragmentActivity)) { - return false; - } - - QMUIFragmentActivity fragmentActivity = (QMUIFragmentActivity) activity; - if (fragmentActivity.getSupportFragmentManager().isStateSaved()) { - // use new activity if the state has already been saved. - return false; - } - - for (Class<? extends QMUIFragmentActivity> aClass : mActivityClsList) { - if (aClass.isAssignableFrom(activity.getClass())) { - return true; - } - } - return false; - } - - private boolean isForceNewActivity(@Nullable Map<String, SchemeValue> scheme) { - if (mForceNewActivity) { - return true; - } - if (scheme == null || scheme.isEmpty()) { - return false; - } - if (scheme.get(QMUISchemeHandler.ARG_FORCE_TO_NEW_ACTIVITY) != null) { - return true; - } - - if (mForceNewActivityKey != null) { - SchemeValue schemeValue = scheme.get(mForceNewActivityKey); - return schemeValue != null && schemeValue.type == Boolean.TYPE && ((Boolean) schemeValue.value); - } - - return false; - } -} diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/FragmentSchemeItem.kt b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/FragmentSchemeItem.kt new file mode 100644 index 000000000..3ff4e6567 --- /dev/null +++ b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/FragmentSchemeItem.kt @@ -0,0 +1,236 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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<Class<out QMUISchemeFragmentFactory>, QMUISchemeFragmentFactory>() +} + +internal class FragmentSchemeItem( + private val fragmentCls: Class<out QMUIFragment?>, + useRefreshIfMatchedCurrent: Boolean, + private val activityClsList: Array<Class<out QMUIFragmentActivity>>, + private val fragmentFactoryCls: Class<out QMUISchemeFragmentFactory>?, + private val forceNewActivity: Boolean, + required: ArrayMap<String, String?>?, + keysForInt: Array<String>?, + keysForBool: Array<String>?, + keysForLong: Array<String>?, + keysForFloat: Array<String>?, + keysForDouble: Array<String>?, + defaultParams: Array<String>?, + schemeMatcherCls: Class<out QMUISchemeMatcher>?, + schemeValueConverterCls: Class<out QMUISchemeValueConverter>? +) : 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<String, SchemeValue>?): 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<out Activity>, + buildingIntent: Intent, + targetActivity: Class<out QMUIFragmentActivity>, + scheme: Map<String, SchemeValue>? + ): Boolean { + if (!targetActivity.isAssignableFrom(buildingActivity)) { + return false + } + val fragmentContainerParam = targetActivity.getAnnotation(FragmentContainerParam::class.java) ?: return true + val required: Array<String> = fragmentContainerParam.required + val any: Array<String> = 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<String, SchemeValue>?): 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) + } +} \ No newline at end of file diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUIDefaultSchemeFragmentFactory.java b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUIDefaultSchemeFragmentFactory.java deleted file mode 100644 index a0f755ac2..000000000 --- a/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUIDefaultSchemeFragmentFactory.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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 androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.qmuiteam.qmui.QMUILog; -import com.qmuiteam.qmui.arch.QMUIFragment; -import com.qmuiteam.qmui.arch.QMUIFragmentActivity; - -import java.util.Map; - -import static com.qmuiteam.qmui.arch.scheme.QMUISchemeHandler.ARG_FROM_SCHEME; - -public class QMUIDefaultSchemeFragmentFactory implements QMUISchemeFragmentFactory { - - @Override - @Nullable - public QMUIFragment factory(@NonNull Class<? extends QMUIFragment> fragmentCls, - @Nullable Map<String, SchemeValue> scheme) { - try { - QMUIFragment fragment = fragmentCls.newInstance(); - fragment.setArguments(createBundleForScheme(scheme)); - return fragment; - } catch (Exception e) { - QMUILog.printErrStackTrace(QMUISchemeHandler.TAG, e, - "Error to create fragment: %s", fragmentCls.getSimpleName()); - return null; - } - } - - @Override - @Nullable - public Intent factory(@NonNull Activity activity, - @NonNull Class<? extends QMUIFragmentActivity>[] activityClassList, - @NonNull Class<? extends QMUIFragment> fragmentCls, - @Nullable Map<String, SchemeValue> scheme) { - Bundle bundle = createBundleForScheme(scheme); - if (activityClassList.length == 0) { - return null; - } - Intent intent = QMUIFragmentActivity.intentOf(activity, activityClassList[0], fragmentCls, bundle); - intent.putExtra(ARG_FROM_SCHEME, true); - return intent; - } - - @NonNull - private Bundle createBundleForScheme(@Nullable Map<String, SchemeValue> scheme) { - Bundle bundle = new Bundle(); - bundle.putBoolean(QMUISchemeHandler.ARG_FROM_SCHEME, true); - if (scheme != null && !scheme.isEmpty()) { - for (Map.Entry<String, SchemeValue> item : scheme.entrySet()) { - String name = item.getKey(); - SchemeValue schemeValue = item.getValue(); - if (schemeValue.type == Integer.TYPE) { - bundle.putInt(name, ((int) schemeValue.value)); - } else if (schemeValue.type == Boolean.TYPE) { - bundle.putBoolean(name, ((boolean) schemeValue.value)); - } else if (schemeValue.type == Long.TYPE) { - bundle.putLong(name, ((long) schemeValue.value)); - } else if (schemeValue.type == Float.TYPE) { - bundle.putFloat(name, ((float) schemeValue.value)); - } else if (schemeValue.type == Double.TYPE) { - bundle.putDouble(name, ((double) schemeValue.value)); - } else { - bundle.putString(name, schemeValue.origin); - } - } - } - return bundle; - } - - @Override - public boolean shouldBlockJump(@NonNull Activity activity, - @NonNull Class<? extends QMUIFragment> fragmentCls, - @Nullable Map<String, SchemeValue> scheme) { - return false; - } -} diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUIDefaultSchemeIntentFactory.java b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUIDefaultSchemeIntentFactory.java deleted file mode 100644 index 261c3dd08..000000000 --- a/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUIDefaultSchemeIntentFactory.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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 androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.util.Map; - -import static com.qmuiteam.qmui.arch.scheme.QMUISchemeHandler.ARG_FROM_SCHEME; - -public class QMUIDefaultSchemeIntentFactory implements QMUISchemeIntentFactory { - @Override - public Intent factory(@NonNull Activity activity, - @NonNull Class<? extends Activity> activityClass, - @Nullable Map<String, SchemeValue> scheme) { - Intent intent = new Intent(activity, activityClass); - intent.putExtra(ARG_FROM_SCHEME, true); - if (scheme != null && !scheme.isEmpty()) { - for (Map.Entry<String, SchemeValue> item : scheme.entrySet()) { - String name = item.getKey(); - SchemeValue schemeValue = item.getValue(); - if (schemeValue.type == Integer.TYPE) { - intent.putExtra(name, ((int) schemeValue.value)); - } else if (schemeValue.type == Boolean.TYPE) { - intent.putExtra(name, ((boolean) schemeValue.value)); - } else if (schemeValue.type == Long.TYPE) { - intent.putExtra(name, ((long) schemeValue.value)); - } else if (schemeValue.type == Float.TYPE) { - intent.putExtra(name, ((float) schemeValue.value)); - } else if (schemeValue.type == Double.TYPE) { - intent.putExtra(name, ((double) schemeValue.value)); - } else { - intent.putExtra(name, schemeValue.origin); - } - } - } - return intent; - } - - @Override - public boolean shouldBlockJump(@NonNull Activity activity, - @NonNull Class<? extends Activity> activityClass, - @Nullable Map<String, SchemeValue> scheme) { - return false; - } -} diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeBuilder.kt b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeBuilder.kt new file mode 100644 index 000000000..a8b24e1f4 --- /dev/null +++ b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeBuilder.kt @@ -0,0 +1,101 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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<String, String>() + parseParamsToMap(params, paramsMap) + if (paramsMap.isNotEmpty()) { + builder.params.putAll(paramsMap) + } + return builder + } + } + + private val params = ArrayMap<String, String>() + + 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() + } +} \ No newline at end of file diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeFragmentFactory.java b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeFragmentFactory.java deleted file mode 100644 index 31f7565dd..000000000 --- a/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeFragmentFactory.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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 androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.qmuiteam.qmui.arch.QMUIFragment; -import com.qmuiteam.qmui.arch.QMUIFragmentActivity; - -import java.util.Map; - -public interface QMUISchemeFragmentFactory { - @Nullable - QMUIFragment factory(@NonNull Class<? extends QMUIFragment> fragmentCls, - @Nullable Map<String, SchemeValue> scheme); - - @Nullable - Intent factory(@NonNull Activity activity, - @NonNull Class<? extends QMUIFragmentActivity>[] activityClassList, - @NonNull Class<? extends QMUIFragment> fragmentCls, - @Nullable Map<String, SchemeValue> scheme); - - boolean shouldBlockJump(@NonNull Activity activity, - @NonNull Class<? extends QMUIFragment> fragmentCls, - @Nullable Map<String, SchemeValue> scheme); -} diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeFragmentFactory.kt b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeFragmentFactory.kt new file mode 100644 index 000000000..61b0e1364 --- /dev/null +++ b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeFragmentFactory.kt @@ -0,0 +1,106 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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<out QMUIFragment>, bundle: Bundle?): QMUIFragment? + fun factory(scheme: Map<String, SchemeValue>?, origin: String): Bundle? + fun proxy(intent: Intent): Intent + + fun startActivities(activity: Activity, intent: List<Intent>, schemeInfo: List<SchemeInfo>) + fun startFragmentAndDestroyCurrent(activity: QMUIFragmentActivity, fragment: QMUIFragment, schemeInfo: SchemeInfo): Int + fun startFragment(activity: QMUIFragmentActivity, fragment: List<QMUIFragment>, schemeInfo: List<SchemeInfo>): Int + fun shouldBlockJump( + activity: Activity, + fragmentCls: Class<out QMUIFragment>, + scheme: Map<String, SchemeValue>? + ): Boolean +} + + +open class QMUIDefaultSchemeFragmentFactory : QMUISchemeFragmentFactory { + override fun factory( + fragmentCls: Class<out QMUIFragment>, + 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<String, SchemeValue>?, 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<Intent>, schemeInfo: List<SchemeInfo>) { + 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<QMUIFragment>, schemeInfo: List<SchemeInfo>): Int { + return activity.startFragments(fragment) + } + + override fun shouldBlockJump( + activity: Activity, + fragmentCls: Class<out QMUIFragment>, + scheme: Map<String, SchemeValue>? + ): Boolean { + return false + } +} \ No newline at end of file diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeHandleInterpolator.java b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeHandleInterpolator.java deleted file mode 100644 index f7734ff11..000000000 --- a/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeHandleInterpolator.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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 androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.util.Map; - -public interface QMUISchemeHandleInterpolator { - boolean intercept(@NonNull QMUISchemeHandler schemeHandler, - @NonNull Activity activity, - @NonNull String action, - @Nullable Map<String, String> params, - @NonNull String origin); -} diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeHandler.java b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeHandler.java deleted file mode 100644 index 4290e6170..000000000 --- a/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeHandler.java +++ /dev/null @@ -1,236 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.Pair; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.qmuiteam.qmui.arch.QMUISwipeBackActivityManager; -import com.qmuiteam.qmui.arch.record.QMUILatestVisitStorage; -import com.qmuiteam.qmui.arch.record.RecordIdClassMap; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class QMUISchemeHandler { - static final String TAG = "QMUISchemeHandler"; - public static String ARG_FROM_SCHEME = "__qmui_arg_from_scheme"; - public static String ARG_FORCE_TO_NEW_ACTIVITY = "__qmui_force_to_new_activity"; - - private static SchemeMap sSchemeMap; - - static { - try { - Class<?> cls = Class.forName(SchemeMap.class.getCanonicalName() + "Impl"); - sSchemeMap = (SchemeMap) cls.newInstance(); - } catch (ClassNotFoundException e) { - sSchemeMap = new SchemeMap() { - @Nullable - @Override - public SchemeItem findScheme(@NonNull String schemeName, @Nullable Map<String, String> params) { - return null; - } - - @Override - public boolean exists(@NonNull String schemeName) { - return false; - } - }; - } catch (IllegalAccessException e) { - throw new RuntimeException("Can not access the Class SchemeMapImpl. " + - "Please file a issue to report this."); - } catch (InstantiationException e) { - throw new RuntimeException("Can not instance the Class SchemeMapImpl. " + - "Please file a issue to report this."); - } - } - - private final String mPrefix; - private final List<QMUISchemeHandleInterpolator> mInterpolatorList; - private final long mBlockSameSchemeTimeout; - private final Class<? extends QMUISchemeIntentFactory> mDefaultIntentFactory; - private final Class<? extends QMUISchemeFragmentFactory> mDefaultFragmentFactory; - - private String mLastHandledScheme = null; - private long mLastSchemeHandledTime = 0; - - private QMUISchemeHandler(Builder builder) { - mPrefix = builder.mPrefix; - List<QMUISchemeHandleInterpolator> interpolatorList = builder.mInterpolatorList; - if(interpolatorList != null && !interpolatorList.isEmpty()){ - mInterpolatorList = new ArrayList<>(interpolatorList); - }else{ - mInterpolatorList = null; - } - mBlockSameSchemeTimeout = builder.mBlockSameSchemeTimeout; - mDefaultIntentFactory = builder.mDefaultIntentFactory; - mDefaultFragmentFactory = builder.mDefaultFragmentFactory; - - } - - public String getPrefix() { - return mPrefix; - } - - public Class<? extends QMUISchemeFragmentFactory> getDefaultFragmentFactory() { - return mDefaultFragmentFactory; - } - - public Class<? extends QMUISchemeIntentFactory> getDefaultIntentFactory() { - return mDefaultIntentFactory; - } - - @Nullable - public SchemeItem getSchemeItem(String action, Map<String, String> params){ - return sSchemeMap.findScheme(action, params); - } - - public boolean handle(String scheme){ - if(scheme == null || !scheme.startsWith(mPrefix)){ - return false; - } - - if(scheme.equals(mLastHandledScheme) && System.currentTimeMillis() - mLastSchemeHandledTime < mBlockSameSchemeTimeout){ - return true; - } - - Activity currentActivity = QMUISwipeBackActivityManager.getInstance().getCurrentActivity(); - if(currentActivity == null){ - return false; - } - - scheme = scheme.substring(mPrefix.length()); - String[] elements = scheme.split("\\?"); - if(elements.length == 0 || elements[0] == null || elements[0].isEmpty()){ - return false; - } - String action = elements[0]; - if(!sSchemeMap.exists(action)){ - return false; - } - - Map<String, String> params; - if(elements.length < 2){ - params = null; - }else{ - params = parseParams(elements[1]); - } - - boolean handled = false; - if(mInterpolatorList != null && !mInterpolatorList.isEmpty()){ - for(QMUISchemeHandleInterpolator interpolator: mInterpolatorList){ - if(interpolator.intercept(this, currentActivity, action, params, scheme)){ - handled = true; - break; - } - } - } - - if(!handled){ - SchemeItem schemeItem = sSchemeMap.findScheme(action, params); - if(schemeItem != null){ - handled = schemeItem.handle(this, currentActivity, schemeItem.convertFrom(params)); - } - } - - if(handled){ - mLastHandledScheme = scheme; - mLastSchemeHandledTime = System.currentTimeMillis(); - } - - return handled; - } - - @Nullable - public Map<String, String> parseParams(@Nullable String schemeParams) { - if (schemeParams == null || schemeParams.isEmpty()) { - return null; - } - - Map<String, String> queryMap = new HashMap<>(); - int start = 0; - do { - int next = schemeParams.indexOf('&', start); - int end = (next == -1) ? schemeParams.length() : next; - if (start == end) { - start += 1; - continue; - } - - int separator = schemeParams.indexOf('=', start); - if (separator > end || separator == -1) { - separator = end; - } - if (separator == start) { - start = end + 1; - continue; - } - - String name = schemeParams.substring(start, separator); - String value = separator == end ? "" : schemeParams.substring(separator + 1, end); - queryMap.put(name, value); - start = end + 1; - } while (start < schemeParams.length()); - return queryMap; - } - - - public static class Builder { - public static final long BLOCK_SAME_SCHEME_DEFAULT_TIMEOUT = 500; - private String mPrefix; - private List<QMUISchemeHandleInterpolator> mInterpolatorList; - private long mBlockSameSchemeTimeout = BLOCK_SAME_SCHEME_DEFAULT_TIMEOUT; - private Class<? extends QMUISchemeIntentFactory> mDefaultIntentFactory = QMUIDefaultSchemeIntentFactory.class; - private Class<? extends QMUISchemeFragmentFactory> mDefaultFragmentFactory = QMUIDefaultSchemeFragmentFactory.class; - - public Builder(@NonNull String prefix) { - mPrefix = prefix; - } - - public Builder addInterpolator(QMUISchemeHandleInterpolator interpolator) { - if(mInterpolatorList == null){ - mInterpolatorList = new ArrayList<>(); - } - mInterpolatorList.add(interpolator); - return this; - } - - public Builder blockSameSchemeTimeout(long blockSameSchemeTimeout) { - mBlockSameSchemeTimeout = blockSameSchemeTimeout; - return this; - } - - public Builder defaultFragmentFactory(Class<? extends QMUISchemeFragmentFactory> defaultFragmentFactory) { - mDefaultFragmentFactory = defaultFragmentFactory; - return this; - } - - public Builder defaultIntentFactory(Class<? extends QMUISchemeIntentFactory> defaultIntentFactory) { - mDefaultIntentFactory = defaultIntentFactory; - return this; - } - - public QMUISchemeHandler build() { - return new QMUISchemeHandler(this); - } - } -} diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeHandler.kt b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeHandler.kt new file mode 100644 index 000000000..5ac27bfb8 --- /dev/null +++ b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeHandler.kt @@ -0,0 +1,201 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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<String, String>?): 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<QMUISchemeHandlerInterceptor> = 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<String>? = null + private var lastSchemeHandledTime: Long = 0 + + + fun getSchemeItem(action: String, params: Map<String, String>?): SchemeItem? { + return sSchemeMap?.findScheme(this, action, params) + } + + fun handle(scheme: String): Boolean { + val list = ArrayList<String>(1) + list.add(scheme) + return handleSchemes(list) + } + + fun handleSchemes(schemes: List<String>): 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<SchemeInfo>(schemes.size) + for (schemeParam in schemes) { + val scheme = schemeParam.substring(prefix.length) + val elements: Array<String?> = scheme.split("\\?".toRegex()).toTypedArray() + val action = elements[0] + if (elements.isEmpty() || action == null || action.isEmpty()) { + return false + } + val params = mutableMapOf<String, String>() + 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<QMUISchemeHandlerInterceptor>() + + var blockSameSchemeTimeout = BLOCK_SAME_SCHEME_DEFAULT_TIMEOUT + var defaultIntentFactory: Class<out QMUISchemeIntentFactory> = QMUIDefaultSchemeIntentFactory::class.java + var defaultFragmentFactory: Class<out QMUISchemeFragmentFactory> = QMUIDefaultSchemeFragmentFactory::class.java + var defaultSchemeMatcher: Class<out QMUISchemeMatcher> = 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 + } + } +} \ No newline at end of file diff --git a/type/src/main/java/com/qmuiteam/qmui/type/element/EffectElement.java b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeHandlerInterceptor.kt similarity index 50% rename from type/src/main/java/com/qmuiteam/qmui/type/element/EffectElement.java rename to arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeHandlerInterceptor.kt index fa2ac7629..53ac02126 100644 --- a/type/src/main/java/com/qmuiteam/qmui/type/element/EffectElement.java +++ b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeHandlerInterceptor.kt @@ -13,37 +13,34 @@ * either express or implied. See the License for the specific language governing permissions and * limitations under the License. */ +package com.qmuiteam.qmui.arch.scheme -package com.qmuiteam.qmui.type.element; +import android.app.Activity +import android.net.Uri -import android.graphics.Canvas; +fun interface QMUISchemeHandlerInterceptor { -import com.qmuiteam.qmui.type.EnvironmentUpdater; -import com.qmuiteam.qmui.type.TypeEnvironment; - -import java.util.List; + fun intercept( + schemeHandler: QMUISchemeHandler, + activity: Activity, + schemes: List<SchemeInfo> + ): Boolean +} -public class EffectElement extends Element { - public EffectElement(final List<Element> list) { - super(' ', null, -1, -1, ""); - addEnvironmentUpdater(new EnvironmentUpdater() { - @Override - public void update(TypeEnvironment env) { - for (Element element : list) { - element.move(env); +class QMUISchemeParamValueDecoder : QMUISchemeHandlerInterceptor { + override fun intercept( + schemeHandler: QMUISchemeHandler, + activity: Activity, + schemes: List<SchemeInfo> + ): Boolean { + for (scheme in schemes) { + for ((key, value) in scheme.params) { + if (value.isNotBlank()) { + scheme.params[key] = Uri.decode(value) } } - }); - } - - @Override - protected void onMeasure(TypeEnvironment env) { - setMeasureDimen(0, 0, 0); + } + return false } - - @Override - protected void onDraw(TypeEnvironment env, Canvas canvas) { - - } -} +} \ No newline at end of file diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeIntentFactory.java b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeIntentFactory.java deleted file mode 100644 index 147a42174..000000000 --- a/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeIntentFactory.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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 androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.util.Map; - -public interface QMUISchemeIntentFactory { - Intent factory(@NonNull Activity activity, - @NonNull Class<? extends Activity> activityClass, - @Nullable Map<String, SchemeValue> scheme); - - boolean shouldBlockJump(@NonNull Activity activity, - @NonNull Class<? extends Activity> activityClass, - @Nullable Map<String, SchemeValue> scheme); -} diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeIntentFactory.kt b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeIntentFactory.kt new file mode 100644 index 000000000..180262953 --- /dev/null +++ b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeIntentFactory.kt @@ -0,0 +1,83 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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<out Activity>, + scheme: Map<String, SchemeValue>?, + origin: String + ): Intent? + + fun startActivities( + activity: Activity, + intent: List<Intent>, + schemeInfo: List<SchemeInfo> + ) + + fun shouldBlockJump( + activity: Activity, + activityClass: Class<out Activity>, + scheme: Map<String, SchemeValue>? + ): Boolean +} + + +open class QMUIDefaultSchemeIntentFactory : QMUISchemeIntentFactory { + override fun factory( + activity: Activity, + activityClass: Class<out Activity>, + scheme: Map<String, SchemeValue>?, + 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<Intent>, schemeInfo: List<SchemeInfo>) { + if (intent.size == 1) { + activity.startActivity(intent[0]) + } else { + activity.startActivities(intent.toTypedArray()) + } + } + + override fun shouldBlockJump( + activity: Activity, + activityClass: Class<out Activity?>, + scheme: Map<String, SchemeValue>? + ): Boolean { + return false + } +} \ No newline at end of file diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/SchemeValue.java b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeMatcher.kt similarity index 67% rename from arch/src/main/java/com/qmuiteam/qmui/arch/scheme/SchemeValue.java rename to arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeMatcher.kt index a4bcbfc59..b40a20090 100644 --- a/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/SchemeValue.java +++ b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeMatcher.kt @@ -13,18 +13,14 @@ * either express or implied. See the License for the specific language governing permissions and * limitations under the License. */ -package com.qmuiteam.qmui.arch.scheme; +package com.qmuiteam.qmui.arch.scheme -import androidx.annotation.NonNull; - -public class SchemeValue { - final String origin; - final Object value; - final Class<?> type; +interface QMUISchemeMatcher { + fun match(schemeItem: SchemeItem, params: Map<String, String?>?): Boolean +} - public SchemeValue(@NonNull String origin, Object value, Class<?> type) { - this.origin = origin; - this.value = value; - this.type = type; +open class QMUIDefaultSchemeMatcher : QMUISchemeMatcher { + override fun match(schemeItem: SchemeItem, params: Map<String, String?>?): Boolean { + return schemeItem.matchRequiredParam(params) } -} +} \ No newline at end of file diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUIUnknownSchemeHandler.kt b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUIUnknownSchemeHandler.kt new file mode 100644 index 000000000..8800ad1c7 --- /dev/null +++ b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUIUnknownSchemeHandler.kt @@ -0,0 +1,5 @@ +package com.qmuiteam.qmui.arch.scheme + +interface QMUIUnknownSchemeHandler { + fun handle(handler: QMUISchemeHandler, handleContext: SchemeHandleContext, schemeInfo: SchemeInfo): Boolean +} \ No newline at end of file diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/SchemeHandleContext.kt b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/SchemeHandleContext.kt new file mode 100644 index 000000000..01d6bfeec --- /dev/null +++ b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/SchemeHandleContext.kt @@ -0,0 +1,188 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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<Intent> = ArrayList() + val fragmentList: MutableList<FragmentAndArg> = ArrayList() + + var buildingIntent: Intent? = null + var buildingActivityClass: Class<out Activity> = activity::class.java + var shouldFinishCurrent = false + + private var schemeIntentFactory: QMUISchemeIntentFactory? = null + private var schemeFragmentFactory: QMUISchemeFragmentFactory? = null + + fun startActivities(schemeInfo: List<SchemeInfo>): 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<out Activity>, 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<Bundle>() + 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<Class<out QMUIFragmentActivity>>, + params: Map<String, SchemeValue>?, + 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<out QMUIFragmentActivity>, + params: Map<String, SchemeValue>? + ): 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<String> = fragmentContainerParam.required + val any: Array<String> = fragmentContainerParam.any + val optional: Array<String> = 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<String, SchemeValue>?, + optional: Array<String> + ) { + 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<out QMUIFragment>, + val arg: Bundle?, + val factory: QMUISchemeFragmentFactory +) diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/SchemeInfo.kt b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/SchemeInfo.kt new file mode 100644 index 000000000..94e277204 --- /dev/null +++ b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/SchemeInfo.kt @@ -0,0 +1,35 @@ +package com.qmuiteam.qmui.arch.scheme + +class SchemeInfo( + val action: String, + val params: MutableMap<String, String>, + val origin: String +) + + +fun parseParamsToMap(schemeParams: String?, queryMap: MutableMap<String, String>) { + 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) +} \ No newline at end of file diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/SchemeItem.java b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/SchemeItem.java deleted file mode 100644 index 623d1a787..000000000 --- a/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/SchemeItem.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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 androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.qmuiteam.qmui.QMUILog; - -import java.util.HashMap; -import java.util.Map; - -public abstract class SchemeItem { - @Nullable - private ArrayMap<String, String> mRequired; - @Nullable - private String[] mKeysForInt; - @Nullable - private String[] mKeysForBool; - @Nullable - private String[] mKeysForLong; - @Nullable - private String[] mKeysForFloat; - @Nullable - private String[] mKeysForDouble; - - public SchemeItem(@Nullable ArrayMap<String, String> required, - @Nullable String[] keysForInt, - @Nullable String[] keysForBool, - @Nullable String[] keysForLong, - @Nullable String[] keysForFloat, - @Nullable String[] keysForDouble) { - mRequired = required; - mKeysForInt = keysForInt; - mKeysForBool = keysForBool; - mKeysForLong = keysForLong; - mKeysForFloat = keysForFloat; - mKeysForDouble = keysForDouble; - } - - @Nullable - public Map<String, SchemeValue> convertFrom(@Nullable Map<String, String> schemeParams) { - if (schemeParams == null || schemeParams.isEmpty()) { - return null; - } - - Map<String, SchemeValue> queryMap = new HashMap<>(); - for(Map.Entry<String, String> param: schemeParams.entrySet()){ - String name = param.getKey(); - String value = param.getValue(); - if(name == null || name.isEmpty()){ - continue; - } - try { - if (contains(mKeysForInt, name)) { - queryMap.put(name, new SchemeValue(value, Integer.valueOf(value), Integer.TYPE)); - } else if (QMUISchemeHandler.ARG_FORCE_TO_NEW_ACTIVITY.equals(name) || contains(mKeysForBool, name)) { - boolean isFalse = "0".equals(value) || "false".equals(value.toLowerCase()); - queryMap.put(name, new SchemeValue(value, !isFalse, Boolean.TYPE)); - } else if (contains(mKeysForLong, name)) { - queryMap.put(name, new SchemeValue(value, Long.valueOf(value), Long.TYPE)); - } else if (contains(mKeysForFloat, name)) { - queryMap.put(name, new SchemeValue(value, Float.valueOf(value), Float.TYPE)); - } else if (contains(mKeysForDouble, name)) { - queryMap.put(name, new SchemeValue(value, Double.valueOf(value), Double.TYPE)); - } else { - queryMap.put(name, new SchemeValue(value, value, String.class)); - } - } catch (Exception e) { - QMUILog.printErrStackTrace(QMUISchemeHandler.TAG, e, - "error to parse scheme param: %s = %s", name, value); - } - } - return queryMap; - } - - private static boolean contains(@Nullable String[] array, @NonNull String key) { - if (array == null || array.length == 0) { - return false; - } - for (int i = 0; i < array.length; i++) { - if (key.equals(array[i])) { - return true; - } - } - return false; - } - - // used by generated code(SchemeMapImpl) - boolean match(@Nullable Map<String, String> scheme) { - if (mRequired == null || mRequired.isEmpty()) { - return true; - } - if (scheme == null || scheme.isEmpty()) { - return false; - } - for (int i = 0; i < mRequired.size(); i++) { - String key = mRequired.keyAt(i); - if(!scheme.containsKey(key)){ - return false; - } - String value = mRequired.valueAt(i); - if(value == null){ - // if no value. that means scheme must provide this key. - continue; - } - String actual = scheme.get(key); - if (actual == null || !actual.equals(value)) { - return false; - } - } - return true; - } - - public abstract boolean handle(@NonNull QMUISchemeHandler handler, - @NonNull Activity activity, - @Nullable Map<String, SchemeValue> scheme); -} diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/SchemeItem.kt b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/SchemeItem.kt new file mode 100644 index 000000000..9b014e84f --- /dev/null +++ b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/SchemeItem.kt @@ -0,0 +1,181 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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<Class<out QMUISchemeMatcher>, QMUISchemeMatcher>() +} +private val schemeValueConverters by lazy { + HashMap<Class<out QMUISchemeValueConverter>, QMUISchemeValueConverter>() +} + +abstract class SchemeItem( + private val required: ArrayMap<String, String?>?, + val isUseRefreshIfMatchedCurrent: Boolean, + private val keysForInt: Array<String>?, + private val keysForBool: Array<String>?, + private val keysForLong: Array<String>?, + private val keysForFloat: Array<String>?, + private val keysForDouble: Array<String>?, + private val defaultParams: Array<String>?, + private val schemeMatcherCls: Class<out QMUISchemeMatcher>?, + private val schemeValueConverterCls: Class<out QMUISchemeValueConverter>? +) { + + fun appendDefaultParams(schemeParams: MutableMap<String, String>?) { + 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<String, String>?): Map<String, SchemeValue>? { + + if (schemeParams == null || schemeParams.isEmpty()) { + return null + } + val queryMap = mutableMapOf<String, SchemeValue>() + 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<String, SchemeValue>?): 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<String, String?>?): Boolean { + val matcher = getSchemeMatcher(handler) + return matcher?.match(this, params) ?: matchRequiredParam(params) + } + + fun matchRequiredParam(params: Map<String, String?>?): 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 +} \ No newline at end of file diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/SchemeMap.java b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/SchemeMap.java deleted file mode 100644 index c33243b69..000000000 --- a/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/SchemeMap.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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 androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.util.Map; - -public interface SchemeMap { - - @Nullable - SchemeItem findScheme(@NonNull String schemeAction, @Nullable Map<String, String> params); - - boolean exists(@NonNull String schemeAction); -} diff --git a/type/src/main/java/com/qmuiteam/qmui/type/element/BreakWordLineElement.java b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/SchemeMap.kt similarity index 74% rename from type/src/main/java/com/qmuiteam/qmui/type/element/BreakWordLineElement.java rename to arch/src/main/java/com/qmuiteam/qmui/arch/scheme/SchemeMap.kt index 628b5b776..709aeefc6 100644 --- a/type/src/main/java/com/qmuiteam/qmui/type/element/BreakWordLineElement.java +++ b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/SchemeMap.kt @@ -13,13 +13,9 @@ * either express or implied. See the License for the specific language governing permissions and * limitations under the License. */ +package com.qmuiteam.qmui.arch.scheme -package com.qmuiteam.qmui.type.element; - -public class BreakWordLineElement extends CharOrPhraseElement { - - public BreakWordLineElement() { - super('-', -1, -1); - setWordPart(WORD_PART_MIDDLE); - } -} +interface SchemeMap { + fun findScheme(handler: QMUISchemeHandler, schemeAction: String, params: Map<String, String>?): SchemeItem? + fun exists(handler: QMUISchemeHandler, schemeAction: String): Boolean +} \ No newline at end of file diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/SchemeRefreshable.kt b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/SchemeRefreshable.kt new file mode 100644 index 000000000..da4a04da7 --- /dev/null +++ b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/SchemeRefreshable.kt @@ -0,0 +1,27 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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?) +} \ No newline at end of file diff --git a/skin-maker-plugin/src/main/groovy/com/qmuiteam/qmui/SkinMakerPlugin.groovy b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/SchemeValue.kt similarity index 61% rename from skin-maker-plugin/src/main/groovy/com/qmuiteam/qmui/SkinMakerPlugin.groovy rename to arch/src/main/java/com/qmuiteam/qmui/arch/scheme/SchemeValue.kt index f483e91a8..f6ea39f52 100644 --- a/skin-maker-plugin/src/main/groovy/com/qmuiteam/qmui/SkinMakerPlugin.groovy +++ b/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/SchemeValue.kt @@ -13,22 +13,20 @@ * either express or implied. See the License for the specific language governing permissions and * limitations under the License. */ +package com.qmuiteam.qmui.arch.scheme -package com.qmuiteam.qmui +class SchemeValue( + val origin: String, + val value: Any, + val type: Class<*> +) -import org.gradle.api.Plugin -import org.gradle.api.Project - -class SkinMakerPlugin implements Plugin<Project> { - - @Override - void apply(Project target) { - def extension = target.extensions.create("skinMaker", SkinMaker.class) - target.android.registerTransform(new SkinMakerTransform(target, extension)) - } - - static class SkinMaker { - File file - } +interface QMUISchemeValueConverter { + fun convert(key: String, originValue: String, schemeParams: Map<String, String?>?): String } +class QMUIDefaultSchemeValueConverter : QMUISchemeValueConverter { + override fun convert(key: String, originValue: String, schemeParams: Map<String, String?>?): String { + return originValue + } +} \ No newline at end of file diff --git a/arch/src/main/res/animator/scale_enter.xml b/arch/src/main/res/animator/scale_enter.xml new file mode 100644 index 000000000..c2d9b453e --- /dev/null +++ b/arch/src/main/res/animator/scale_enter.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<set xmlns:android="http://schemas.android.com/apk/res/android"> + <objectAnimator + android:interpolator="@android:interpolator/decelerate_cubic" + android:propertyName="scaleX" + android:valueFrom="0.9" android:valueTo="1.0" + android:duration="@integer/qmui_anim_duration"/> + + <objectAnimator + android:interpolator="@android:interpolator/decelerate_cubic" + android:propertyName="scaleY" + android:valueFrom="0.9" android:valueTo="1.0" + android:duration="@integer/qmui_anim_duration"/> + + <objectAnimator + android:interpolator="@android:interpolator/decelerate_cubic" + android:propertyName="alpha" + android:valueFrom="0.0" android:valueTo="1.0" + android:duration="@integer/qmui_anim_duration"/> +</set> \ No newline at end of file diff --git a/arch/src/main/res/animator/scale_exit.xml b/arch/src/main/res/animator/scale_exit.xml new file mode 100644 index 000000000..470803657 --- /dev/null +++ b/arch/src/main/res/animator/scale_exit.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Tencent is pleased to support the open source community by making QMUI_Android available. + + Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + + Licensed under the MIT License (the "License"); you may not use this file except in + compliance with the License. You may obtain a copy of the License at + + http://opensource.org/licenses/MIT + + Unless required by applicable law or agreed to in writing, software distributed under the License is + distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + either express or implied. See the License for the specific language governing permissions and + limitations under the License. +--> + +<set xmlns:android="http://schemas.android.com/apk/res/android" + android:background="@android:color/transparent"> + + <objectAnimator + android:interpolator="@android:interpolator/decelerate_quad" + android:propertyName="scaleX" + android:valueFrom="1.0" android:valueTo="0.9" + android:duration="@integer/qmui_anim_duration"/> + + <objectAnimator + android:interpolator="@android:interpolator/decelerate_quad" + android:propertyName="scaleY" + android:valueFrom="1.0" android:valueTo="0.9" + android:duration="@integer/qmui_anim_duration"/> + + <objectAnimator + android:interpolator="@android:interpolator/decelerate_quad" + android:propertyName="alpha" + android:valueFrom="1.0" android:valueTo="0.0" + android:duration="@integer/qmui_anim_duration"/> +</set> \ No newline at end of file diff --git a/arch/src/main/res/animator/slide_in_left.xml b/arch/src/main/res/animator/slide_in_left.xml new file mode 100644 index 000000000..d0ce44e1e --- /dev/null +++ b/arch/src/main/res/animator/slide_in_left.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Tencent is pleased to support the open source community by making QMUI_Android available. + + Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + + Licensed under the MIT License (the "License"); you may not use this file except in + compliance with the License. You may obtain a copy of the License at + + http://opensource.org/licenses/MIT + + Unless required by applicable law or agreed to in writing, software distributed under the License is + distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + either express or implied. See the License for the specific language governing permissions and + limitations under the License. +--> + +<set xmlns:android="http://schemas.android.com/apk/res/android"> + + <objectAnimator + android:interpolator="@android:interpolator/decelerate_cubic" + android:propertyName="xFraction" + android:valueFrom="-0.4" android:valueTo="0" + android:valueType="floatType" + android:duration="@integer/qmui_anim_duration"/> + + <objectAnimator + android:interpolator="@android:interpolator/decelerate_cubic" + android:propertyName="alpha" + android:valueFrom="0.85" android:valueTo="1.0" + android:duration="@integer/qmui_anim_duration"/> + +</set> \ No newline at end of file diff --git a/qmui/lint.xml b/arch/src/main/res/animator/slide_in_right.xml similarity index 60% rename from qmui/lint.xml rename to arch/src/main/res/animator/slide_in_right.xml index 0e8bde689..207b8e68e 100644 --- a/qmui/lint.xml +++ b/arch/src/main/res/animator/slide_in_right.xml @@ -1,4 +1,4 @@ -<?xml version="1.0" encoding="UTF-8"?> +<?xml version="1.0" encoding="utf-8"?> <!-- Tencent is pleased to support the open source community by making QMUI_Android available. @@ -15,14 +15,12 @@ limitations under the License. --> -<lint> - <!-- Disable the given check in this project --> - <issue id="HardcodedText" severity="ignore"/> - <issue id="SmallSp" severity="ignore"/> - <issue id="IconMissingDensityFolder" severity="ignore"/> - <issue id="RtlHardcoded" severity="ignore"/> - <issue id="Deprecated" severity="warning"> - <ignore regexp="singleLine"/> - </issue> - <issue id="RtlSymmetry" severity="ignore"/> -</lint> +<set xmlns:android="http://schemas.android.com/apk/res/android"> + + <objectAnimator + android:interpolator="@android:interpolator/decelerate_cubic" + android:propertyName="xFraction" + android:valueFrom="1.0" android:valueTo="0.0" + android:valueType="floatType" + android:duration="@integer/qmui_anim_duration"/> +</set> \ No newline at end of file diff --git a/arch/src/main/res/animator/slide_out_left.xml b/arch/src/main/res/animator/slide_out_left.xml new file mode 100644 index 000000000..3caabe855 --- /dev/null +++ b/arch/src/main/res/animator/slide_out_left.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Tencent is pleased to support the open source community by making QMUI_Android available. + + Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + + Licensed under the MIT License (the "License"); you may not use this file except in + compliance with the License. You may obtain a copy of the License at + + http://opensource.org/licenses/MIT + + Unless required by applicable law or agreed to in writing, software distributed under the License is + distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + either express or implied. See the License for the specific language governing permissions and + limitations under the License. +--> + +<set xmlns:android="http://schemas.android.com/apk/res/android"> + + <objectAnimator + android:interpolator="@android:interpolator/decelerate_quad" + android:propertyName="xFraction" + android:valueFrom="0.0" android:valueTo="-1.0" + android:valueType="floatType" + android:duration="@integer/qmui_anim_duration"/> + + <objectAnimator + android:interpolator="@android:interpolator/decelerate_quad" + android:propertyName="alpha" + android:valueFrom="1.0" android:valueTo="0.85" + android:duration="@integer/qmui_anim_duration"/> +</set> \ No newline at end of file diff --git a/arch/src/main/res/animator/slide_out_right.xml b/arch/src/main/res/animator/slide_out_right.xml new file mode 100644 index 000000000..19708b1a0 --- /dev/null +++ b/arch/src/main/res/animator/slide_out_right.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Tencent is pleased to support the open source community by making QMUI_Android available. + + Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + + Licensed under the MIT License (the "License"); you may not use this file except in + compliance with the License. You may obtain a copy of the License at + + http://opensource.org/licenses/MIT + + Unless required by applicable law or agreed to in writing, software distributed under the License is + distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + either express or implied. See the License for the specific language governing permissions and + limitations under the License. +--> + +<set xmlns:android="http://schemas.android.com/apk/res/android"> + + <objectAnimator + android:interpolator="@android:interpolator/decelerate_quad" + android:propertyName="xFraction" + android:valueFrom="0.0" android:valueTo="1.0" + android:valueType="floatType" + android:duration="@integer/qmui_anim_duration"/> + +</set> \ No newline at end of file diff --git a/skin-maker/src/main/res/values/strings.xml b/arch/src/main/res/animator/slide_still.xml similarity index 67% rename from skin-maker/src/main/res/values/strings.xml rename to arch/src/main/res/animator/slide_still.xml index 4527cba8d..008073fab 100644 --- a/skin-maker/src/main/res/values/strings.xml +++ b/arch/src/main/res/animator/slide_still.xml @@ -15,7 +15,13 @@ limitations under the License. --> -<resources> - <string name="app_name">skin-maker</string> - <string name="app_new_attr">New Attr</string> -</resources> +<set xmlns:android="http://schemas.android.com/apk/res/android"> + + <objectAnimator + android:interpolator="@android:interpolator/decelerate_quad" + android:propertyName="x" + android:valueFrom="0.0" android:valueTo="0.0" + android:valueType="floatType" + android:duration="@integer/qmui_anim_duration"/> + +</set> \ No newline at end of file diff --git a/arch/src/main/res/values/ids.xml b/arch/src/main/res/values/ids.xml index b3d919d88..0f7d83f2e 100644 --- a/arch/src/main/res/values/ids.xml +++ b/arch/src/main/res/values/ids.xml @@ -16,6 +16,7 @@ --> <resources> + <item name="qmui_activity_root_id" type="id"/> <item name="qmui_activity_fragment_container_id" type="id"/> <item name="qmui_nav_fragment_container_id" type="id"/> diff --git a/arch/src/test/java/com/qmuiteam/qmui/arch/ExampleUnitTest.java b/arch/src/test/java/com/qmuiteam/qmui/arch/ExampleUnitTest.java index b5ce7cd1f..f27999087 100644 --- a/arch/src/test/java/com/qmuiteam/qmui/arch/ExampleUnitTest.java +++ b/arch/src/test/java/com/qmuiteam/qmui/arch/ExampleUnitTest.java @@ -1,8 +1,5 @@ package com.qmuiteam.qmui.arch; -import org.junit.Test; - -import static org.junit.Assert.*; /** * Example local unit test, which will execute on the development machine (host). @@ -10,8 +7,5 @@ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a> */ public class ExampleUnitTest { - @Test - public void addition_isCorrect() throws Exception { - assertEquals(4, 2 + 2); - } + } \ No newline at end of file diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 5fb48b2aa..000000000 --- a/build.gradle +++ /dev/null @@ -1,41 +0,0 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. - -buildscript { - ext.kotlin_version = '1.3.61' - repositories { - mavenLocal() - google() - jcenter() - } - dependencies { - classpath 'com.android.tools.build:gradle:3.6.1' - classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" -// classpath 'com.qmuiteam:skin-maker-plugin:0.0.1' - } -} - -subprojects { project -> - group = GROUP -} - -allprojects { - repositories { - jcenter() - mavenLocal() - google() - } - - ext { - minSdkVersion = 19 - targetSdkVersion = 29 - compileSdkVersion = 29 - appcompatVersion= '1.2.0-alpha03' - materialVersion='1.1.0' - annotationVersion='1.1.0-beta01' - butterknifeVersion = '10.1.0' - constraintLayoutVersion = "1.1.3" - mmkvVersion = '1.0.23' - junitVersion='4.13' - } -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 000000000..e5fcb871d --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,31 @@ +// 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() + } +} diff --git a/compiler/build.gradle b/compiler/build.gradle deleted file mode 100644 index 3a34fabbb..000000000 --- a/compiler/build.gradle +++ /dev/null @@ -1,12 +0,0 @@ -apply plugin: 'java-library' - -dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation project(':lib') - implementation 'com.squareup:javapoet:1.10.0' - implementation 'com.google.auto.service:auto-service:1.0-rc2' - annotationProcessor 'com.google.auto.service:auto-service:1.0-rc2' -} - -sourceCompatibility = "1.7" -targetCompatibility = "1.7" diff --git a/compiler/build.gradle.kts b/compiler/build.gradle.kts new file mode 100644 index 000000000..4dc88e878 --- /dev/null +++ b/compiler/build.gradle.kts @@ -0,0 +1,16 @@ +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) +} diff --git a/compiler/src/main/java/com/qmuiteam/qmuidemo/compiler/WidgetProcessor.java b/compiler/src/main/java/com/qmuiteam/qmuidemo/compiler/WidgetProcessor.java index 1056b1a34..8d0a727d3 100644 --- a/compiler/src/main/java/com/qmuiteam/qmuidemo/compiler/WidgetProcessor.java +++ b/compiler/src/main/java/com/qmuiteam/qmuidemo/compiler/WidgetProcessor.java @@ -17,6 +17,7 @@ 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; @@ -25,7 +26,6 @@ import com.squareup.javapoet.TypeName; import com.squareup.javapoet.TypeSpec; import com.squareup.javapoet.WildcardTypeName; -import com.qmuiteam.qmuidemo.lib.annotation.Widget; import java.io.IOException; import java.util.LinkedHashSet; diff --git a/compose-core/.gitignore b/compose-core/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/compose-core/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/compose-core/build.gradle.kts b/compose-core/build.gradle.kts new file mode 100644 index 000000000..f90b7725e --- /dev/null +++ b/compose-core/build.gradle.kts @@ -0,0 +1,52 @@ + +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) +} \ No newline at end of file diff --git a/skin-maker/proguard-rules.pro b/compose-core/proguard-rules.pro similarity index 94% rename from skin-maker/proguard-rules.pro rename to compose-core/proguard-rules.pro index f1b424510..481bb4348 100644 --- a/skin-maker/proguard-rules.pro +++ b/compose-core/proguard-rules.pro @@ -18,4 +18,4 @@ # If you keep the line number information, uncomment this to # hide the original source file name. -#-renamesourcefileattribute SourceFile +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/compose-core/src/androidTest/java/com/qmuiteam/compose/ExampleInstrumentedTest.kt b/compose-core/src/androidTest/java/com/qmuiteam/compose/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..b0a4768b4 --- /dev/null +++ b/compose-core/src/androidTest/java/com/qmuiteam/compose/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +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) + } +} \ No newline at end of file diff --git a/compose-core/src/main/AndroidManifest.xml b/compose-core/src/main/AndroidManifest.xml new file mode 100644 index 000000000..6a980b08e --- /dev/null +++ b/compose-core/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.qmuiteam.compose.core"> + +</manifest> \ No newline at end of file diff --git a/compose-core/src/main/java/com/qmuiteam/compose/core/ex/DrawScopeEx.kt b/compose-core/src/main/java/com/qmuiteam/compose/core/ex/DrawScopeEx.kt new file mode 100644 index 000000000..c6961ce25 --- /dev/null +++ b/compose-core/src/main/java/com/qmuiteam/compose/core/ex/DrawScopeEx.kt @@ -0,0 +1,45 @@ +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 + ) +} \ No newline at end of file diff --git a/compose-core/src/main/java/com/qmuiteam/compose/core/helper/Dimen.kt b/compose-core/src/main/java/com/qmuiteam/compose/core/helper/Dimen.kt new file mode 100644 index 000000000..a09123a92 --- /dev/null +++ b/compose-core/src/main/java/com/qmuiteam/compose/core/helper/Dimen.kt @@ -0,0 +1,11 @@ +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 +} \ No newline at end of file diff --git a/compose-core/src/main/java/com/qmuiteam/compose/core/helper/Global.kt b/compose-core/src/main/java/com/qmuiteam/compose/core/helper/Global.kt new file mode 100644 index 000000000..26b849f8f --- /dev/null +++ b/compose-core/src/main/java/com/qmuiteam/compose/core/helper/Global.kt @@ -0,0 +1,5 @@ +package com.qmuiteam.compose.core.helper + +object QMUIGlobal { + var debug: Boolean = false +} \ No newline at end of file diff --git a/compose-core/src/main/java/com/qmuiteam/compose/core/helper/Log.kt b/compose-core/src/main/java/com/qmuiteam/compose/core/helper/Log.kt new file mode 100644 index 000000000..dc16f1daa --- /dev/null +++ b/compose-core/src/main/java/com/qmuiteam/compose/core/helper/Log.kt @@ -0,0 +1,50 @@ +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) + } +} \ No newline at end of file diff --git a/compose-core/src/main/java/com/qmuiteam/compose/core/helper/LogTag.kt b/compose-core/src/main/java/com/qmuiteam/compose/core/helper/LogTag.kt new file mode 100644 index 000000000..553dc1de2 --- /dev/null +++ b/compose-core/src/main/java/com/qmuiteam/compose/core/helper/LogTag.kt @@ -0,0 +1,21 @@ +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 <reified T: Any> 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) + } +} \ No newline at end of file diff --git a/compose-core/src/main/java/com/qmuiteam/compose/core/provider/WindowInsets.kt b/compose-core/src/main/java/com/qmuiteam/compose/core/provider/WindowInsets.kt new file mode 100644 index 000000000..1a783894d --- /dev/null +++ b/compose-core/src/main/java/com/qmuiteam/compose/core/provider/WindowInsets.kt @@ -0,0 +1,54 @@ +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 + ) + } +} \ No newline at end of file diff --git a/compose-core/src/main/java/com/qmuiteam/compose/core/ui/DefaultConfig.kt b/compose-core/src/main/java/com/qmuiteam/compose/core/ui/DefaultConfig.kt new file mode 100644 index 000000000..e23e70f46 --- /dev/null +++ b/compose-core/src/main/java/com/qmuiteam/compose/core/ui/DefaultConfig.kt @@ -0,0 +1,24 @@ +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 + + + diff --git a/compose-core/src/main/java/com/qmuiteam/compose/core/ui/PressWithAlphaBox.kt b/compose-core/src/main/java/com/qmuiteam/compose/core/ui/PressWithAlphaBox.kt new file mode 100644 index 000000000..8a6efdab4 --- /dev/null +++ b/compose-core/src/main/java/com/qmuiteam/compose/core/ui/PressWithAlphaBox.kt @@ -0,0 +1,33 @@ +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 + ) + +} \ No newline at end of file diff --git a/compose-core/src/main/java/com/qmuiteam/compose/core/ui/QMUIIcon.kt b/compose-core/src/main/java/com/qmuiteam/compose/core/ui/QMUIIcon.kt new file mode 100644 index 000000000..2d3ab93a9 --- /dev/null +++ b/compose-core/src/main/java/com/qmuiteam/compose/core/ui/QMUIIcon.kt @@ -0,0 +1,117 @@ +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 + ) +} \ No newline at end of file diff --git a/compose-core/src/main/java/com/qmuiteam/compose/core/ui/QMUIItem.kt b/compose-core/src/main/java/com/qmuiteam/compose/core/ui/QMUIItem.kt new file mode 100644 index 000000000..476126a3b --- /dev/null +++ b/compose-core/src/main/java/com/qmuiteam/compose/core/ui/QMUIItem.kt @@ -0,0 +1,97 @@ +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) + } +} \ No newline at end of file diff --git a/compose-core/src/main/java/com/qmuiteam/compose/core/ui/QMUITopBar.kt b/compose-core/src/main/java/com/qmuiteam/compose/core/ui/QMUITopBar.kt new file mode 100644 index 000000000..ea648c7b1 --- /dev/null +++ b/compose-core/src/main/java/com/qmuiteam/compose/core/ui/QMUITopBar.kt @@ -0,0 +1,413 @@ +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<QMUITopBarItem> = emptyList(), + rightItems: List<QMUITopBarItem> = 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<QMUITopBarItem> = emptyList(), + rightItems: List<QMUITopBarItem> = 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<QMUITopBarItem> = emptyList(), + rightItems: List<QMUITopBarItem> = 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)" +} \ No newline at end of file diff --git a/compose-core/src/main/res/drawable/ic_qmui_checkbox_checked.xml b/compose-core/src/main/res/drawable/ic_qmui_checkbox_checked.xml new file mode 100644 index 000000000..239aa91b7 --- /dev/null +++ b/compose-core/src/main/res/drawable/ic_qmui_checkbox_checked.xml @@ -0,0 +1,12 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="18dp" + android:height="18dp" + android:viewportWidth="18" + android:viewportHeight="18"> + <path + android:pathData="M9,0C13.9706,0 18,4.0294 18,9C18,13.9706 13.9706,18 9,18C4.0294,18 0,13.9706 0,9C0,4.0294 4.0294,0 9,0ZM12,6L7.5,10.5L6,9L4.5,10.5L7.5,13.5L13.5,7.5L12,6Z" + android:strokeWidth="1" + android:fillColor="#FF00A8E1" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> +</vector> diff --git a/compose-core/src/main/res/drawable/ic_qmui_checkbox_normal.xml b/compose-core/src/main/res/drawable/ic_qmui_checkbox_normal.xml new file mode 100644 index 000000000..354ca8ab7 --- /dev/null +++ b/compose-core/src/main/res/drawable/ic_qmui_checkbox_normal.xml @@ -0,0 +1,12 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="18dp" + android:height="18dp" + android:viewportWidth="18" + android:viewportHeight="18"> + <path + android:pathData="M9,0C13.9706,0 18,4.0294 18,9C18,13.9706 13.9706,18 9,18C4.0294,18 0,13.9706 0,9C0,4.0294 4.0294,0 9,0ZM9,1.5C4.8579,1.5 1.5,4.8579 1.5,9C1.5,13.1421 4.8579,16.5 9,16.5C13.1421,16.5 16.5,13.1421 16.5,9C16.5,4.8579 13.1421,1.5 9,1.5Z" + android:strokeWidth="1" + android:fillColor="#CCCCCC" + android:fillType="nonZero" + android:strokeColor="#00000000"/> +</vector> diff --git a/compose-core/src/main/res/drawable/ic_qmui_checkbox_partial.xml b/compose-core/src/main/res/drawable/ic_qmui_checkbox_partial.xml new file mode 100644 index 000000000..1deba276b --- /dev/null +++ b/compose-core/src/main/res/drawable/ic_qmui_checkbox_partial.xml @@ -0,0 +1,12 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="18dp" + android:height="18dp" + android:viewportWidth="18" + android:viewportHeight="18"> + <path + android:fillColor="#FF00A8E1" + android:fillType="evenOdd" + android:pathData="M9,18C13.9706,18 18,13.9706 18,9C18,4.0294 13.9706,0 9,0C4.0294,0 0,4.0294 0,9C0,13.9706 4.0294,18 9,18ZM4,8L14,8L14,10L4,10L4,8Z" + android:strokeWidth="1" + android:strokeColor="#00000000" /> +</vector> diff --git a/compose-core/src/main/res/drawable/ic_qmui_chevron.xml b/compose-core/src/main/res/drawable/ic_qmui_chevron.xml new file mode 100644 index 000000000..114badc86 --- /dev/null +++ b/compose-core/src/main/res/drawable/ic_qmui_chevron.xml @@ -0,0 +1,12 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="6dp" + android:height="9dp" + android:viewportWidth="6" + android:viewportHeight="9"> + <path + android:pathData="M3.11747,4.5l-3.11747,3.5109l1.14712,0.9891l3.99574,-4.5l-3.99574,-4.5l-1.14712,0.9891z" + android:strokeWidth="1" + android:fillColor="#666666" + android:fillType="nonZero" + android:strokeColor="#00000000"/> +</vector> diff --git a/compose-core/src/main/res/drawable/ic_qmui_mark.xml b/compose-core/src/main/res/drawable/ic_qmui_mark.xml new file mode 100644 index 000000000..bcf5221ae --- /dev/null +++ b/compose-core/src/main/res/drawable/ic_qmui_mark.xml @@ -0,0 +1,12 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="12dp" + android:height="12dp" + android:viewportWidth="12" + android:viewportHeight="12"> + <path + android:pathData="M0,7l1.5,-1.5l2.5,2.5l6,-6l1.5,1.5l-7.5,7.5z" + android:strokeWidth="1" + android:fillColor="#FF00A8E1" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> +</vector> diff --git a/compose-core/src/main/res/drawable/ic_qmui_topbar_back.xml b/compose-core/src/main/res/drawable/ic_qmui_topbar_back.xml new file mode 100644 index 000000000..e3f719ac1 --- /dev/null +++ b/compose-core/src/main/res/drawable/ic_qmui_topbar_back.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Tencent is pleased to support the open source community by making QMUI_Android available. + + Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + + Licensed under the MIT License (the "License"); you may not use this file except in + compliance with the License. You may obtain a copy of the License at + + http://opensource.org/licenses/MIT + + Unless required by applicable law or agreed to in writing, software distributed under the License is + distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + either express or implied. See the License for the specific language governing permissions and + limitations under the License. +--> + + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:pathData="M20,11L7.8,11l5.6,-5.6L12,4l-8,8l8,8l1.4,-1.4L7.8,13L20,13L20,11z" + android:fillColor="#ffffff"/> +</vector> diff --git a/compose-core/src/main/res/values/qmui_ids.xml b/compose-core/src/main/res/values/qmui_ids.xml new file mode 100644 index 000000000..e13836a67 --- /dev/null +++ b/compose-core/src/main/res/values/qmui_ids.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <item name="qmui_window_inset_cache" type="id"/> +</resources> \ No newline at end of file diff --git a/compose-core/src/test/java/com/qmuiteam/compose/ExampleUnitTest.kt b/compose-core/src/test/java/com/qmuiteam/compose/ExampleUnitTest.kt new file mode 100644 index 000000000..83f698e02 --- /dev/null +++ b/compose-core/src/test/java/com/qmuiteam/compose/ExampleUnitTest.kt @@ -0,0 +1,9 @@ +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 { +} \ No newline at end of file diff --git a/compose/.gitignore b/compose/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/compose/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/compose/build.gradle.kts b/compose/build.gradle.kts new file mode 100644 index 000000000..9853088e6 --- /dev/null +++ b/compose/build.gradle.kts @@ -0,0 +1,49 @@ + +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) +} \ No newline at end of file diff --git a/compose/proguard-rules.pro b/compose/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/compose/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/compose/src/androidTest/java/com/qmuiteam/compose/ExampleInstrumentedTest.kt b/compose/src/androidTest/java/com/qmuiteam/compose/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..b0a4768b4 --- /dev/null +++ b/compose/src/androidTest/java/com/qmuiteam/compose/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +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) + } +} \ No newline at end of file diff --git a/compose/src/main/AndroidManifest.xml b/compose/src/main/AndroidManifest.xml new file mode 100644 index 000000000..c2d442cf5 --- /dev/null +++ b/compose/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.qmuiteam.compose"> + +</manifest> \ No newline at end of file diff --git a/compose/src/main/java/com/qmuiteam/compose/modal/ModalImpl.kt b/compose/src/main/java/com/qmuiteam/compose/modal/ModalImpl.kt new file mode 100644 index 000000000..f4aff9cf3 --- /dev/null +++ b/compose/src/main/java/com/qmuiteam/compose/modal/ModalImpl.kt @@ -0,0 +1,213 @@ +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<QMUIModal.Action>() + private val onDismissListeners = arrayListOf<QMUIModal.Action>() + 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() + } + } + } + } +} + diff --git a/compose/src/main/java/com/qmuiteam/compose/modal/QMUIBottomSheet.kt b/compose/src/main/java/com/qmuiteam/compose/modal/QMUIBottomSheet.kt new file mode 100644 index 000000000..a0441902f --- /dev/null +++ b/compose/src/main/java/com/qmuiteam/compose/modal/QMUIBottomSheet.kt @@ -0,0 +1,268 @@ +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<Float>, + 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 + } +} diff --git a/compose/src/main/java/com/qmuiteam/compose/modal/QMUIDialog.kt b/compose/src/main/java/com/qmuiteam/compose/modal/QMUIDialog.kt new file mode 100644 index 000000000..e702a7d1d --- /dev/null +++ b/compose/src/main/java/com/qmuiteam/compose/modal/QMUIDialog.kt @@ -0,0 +1,370 @@ +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<QMUIModalAction> +){ + 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<QMUIModalAction> +) { + 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<String>, + 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<String>, + checked: Set<Int>, + disabled: Set<Int> = 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) + } +} \ No newline at end of file diff --git a/compose/src/main/java/com/qmuiteam/compose/modal/QMUIModal.kt b/compose/src/main/java/com/qmuiteam/compose/modal/QMUIModal.kt new file mode 100644 index 000000000..60652b55e --- /dev/null +++ b/compose/src/main/java/com/qmuiteam/compose/modal/QMUIModal.kt @@ -0,0 +1,176 @@ +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<Long, QMUIModal>() +} + +@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<FrameLayout, OnBackPressedDispatcher> +} + +class ActivityHostModalProvider : ModalHostProvider { + override fun provide(view: View): Pair<FrameLayout, OnBackPressedDispatcher> { + val contentLayout = + view.rootView.findViewById<FrameLayout>(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) + } + } +} \ No newline at end of file diff --git a/compose/src/main/java/com/qmuiteam/compose/modal/QMUIToast.kt b/compose/src/main/java/com/qmuiteam/compose/modal/QMUIToast.kt new file mode 100644 index 000000000..aa60bb3dc --- /dev/null +++ b/compose/src/main/java/com/qmuiteam/compose/modal/QMUIToast.kt @@ -0,0 +1,200 @@ +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() +} \ No newline at end of file diff --git a/compose/src/main/res/values/ids.xml b/compose/src/main/res/values/ids.xml new file mode 100644 index 000000000..a402c628b --- /dev/null +++ b/compose/src/main/res/values/ids.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <item name="qmui_modals" type="id"/> +</resources> \ No newline at end of file diff --git a/compose/src/test/java/com/qmuiteam/compose/ExampleUnitTest.kt b/compose/src/test/java/com/qmuiteam/compose/ExampleUnitTest.kt new file mode 100644 index 000000000..74f12c93a --- /dev/null +++ b/compose/src/test/java/com/qmuiteam/compose/ExampleUnitTest.kt @@ -0,0 +1,10 @@ +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 { + +} \ No newline at end of file diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 000000000..432c6d264 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,49 @@ +#!/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 \ No newline at end of file diff --git a/skin-maker-plugin/.gitignore b/editor/.gitignore similarity index 100% rename from skin-maker-plugin/.gitignore rename to editor/.gitignore diff --git a/editor/build.gradle.kts b/editor/build.gradle.kts new file mode 100644 index 000000000..4fe03683b --- /dev/null +++ b/editor/build.gradle.kts @@ -0,0 +1,49 @@ +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")) +} \ No newline at end of file diff --git a/skin-maker/consumer-rules.pro b/editor/consumer-rules.pro similarity index 100% rename from skin-maker/consumer-rules.pro rename to editor/consumer-rules.pro diff --git a/editor/proguard-rules.pro b/editor/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/editor/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/editor/src/main/AndroidManifest.xml b/editor/src/main/AndroidManifest.xml new file mode 100644 index 000000000..2b09b9d46 --- /dev/null +++ b/editor/src/main/AndroidManifest.xml @@ -0,0 +1 @@ +<manifest package="com.qmuiteam.editor.editor"/> diff --git a/editor/src/main/java/com/qmuiteam/editor/EditorBehavior.kt b/editor/src/main/java/com/qmuiteam/editor/EditorBehavior.kt new file mode 100644 index 000000000..ebe446ae7 --- /dev/null +++ b/editor/src/main/java/com/qmuiteam/editor/EditorBehavior.kt @@ -0,0 +1,99 @@ +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) +} diff --git a/editor/src/main/java/com/qmuiteam/editor/QMUIEditor.kt b/editor/src/main/java/com/qmuiteam/editor/QMUIEditor.kt new file mode 100644 index 000000000..7fd5ca723 --- /dev/null +++ b/editor/src/main/java/com/qmuiteam/editor/QMUIEditor.kt @@ -0,0 +1,315 @@ +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<EditorBehavior>, + 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<EditorDecoration>()) + } + + 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<EditorDecoration>() + 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<MutableRange<SpanStyle>>() + val mutableParagraph = mutableListOf<MutableRange<ParagraphStyle>>() + 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<MutableRange<SpanStyle>>() + val normalSpans = mutableListOf<MutableRange<SpanStyle>>() + 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 + ) +} \ No newline at end of file diff --git a/editor/src/main/java/com/qmuiteam/editor/Range.kt b/editor/src/main/java/com/qmuiteam/editor/Range.kt new file mode 100644 index 000000000..54f75ec0a --- /dev/null +++ b/editor/src/main/java/com/qmuiteam/editor/Range.kt @@ -0,0 +1,66 @@ +package com.qmuiteam.editor + +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextRange + +internal class MutableRange<T>( + 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 <T> AnnotatedString.Range<T>.isCursorContained(cursorPos: Int): Boolean{ + return if(start == end){ + start == cursorPos + } else { + cursorPos in (start + 1)..end + } +} \ No newline at end of file diff --git a/editor/src/main/java/com/qmuiteam/editor/TextFieldValueEx.kt b/editor/src/main/java/com/qmuiteam/editor/TextFieldValueEx.kt new file mode 100644 index 000000000..e1efe70df --- /dev/null +++ b/editor/src/main/java/com/qmuiteam/editor/TextFieldValueEx.kt @@ -0,0 +1,249 @@ +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<MutableRange<SpanStyle>>) -> Unit +): TextFieldValue { + val mutableSpans = mutableListOf<MutableRange<SpanStyle>>() + 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<ParagraphStyle>.replaceStyleIfNeeded( + value: TextFieldValue, + tag: String, + style: ParagraphStyle +): AnnotatedString.Range<ParagraphStyle>? { + 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<AnnotatedString.Range<ParagraphStyle>>() + 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<MutableRange<ParagraphStyle>>) -> Unit +): TextFieldValue { + val mutableParagraphs = mutableListOf<MutableRange<ParagraphStyle>>() + 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<AnnotatedString.Range<ParagraphStyle>>() + 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 + ) +} \ No newline at end of file diff --git a/editor/src/main/java/com/qmuiteam/editor/WordEdit.kt b/editor/src/main/java/com/qmuiteam/editor/WordEdit.kt new file mode 100644 index 000000000..51ee11bd4 --- /dev/null +++ b/editor/src/main/java/com/qmuiteam/editor/WordEdit.kt @@ -0,0 +1,159 @@ +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<WordEditPoint> +) + +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<WordEditRecord>(oldText.length + 1) + val next = arrayOfNulls<WordEditRecord>(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<WordEditPoint>() + var node = ret.node + while (node != null) { + list.addFirst(node!!.point) + node = node?.prev + } + return WordEditResult(ret.dis, list) +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index ffe18a0b6..8fbc68510 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,7 @@ # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx1536m +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 @@ -19,13 +19,14 @@ org.gradle.jvmargs=-Xmx1536m android.injected.testOnly=false android.useAndroidX=true -android.enableJetifier=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.0-alpha09 -QMUI_ARCH_VERSION=2.0.0-alpha09 -QMUI_LINT_VERSION = 1.1.0 -QMUI_SKIN_MAKER_VERSION = 0.0.1 -QMUI_TYPE_VERSION = 0.0.1 +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 \ No newline at end of file diff --git a/gradle/deploy.gradle b/gradle/deploy.gradle deleted file mode 100644 index af7a37343..000000000 --- a/gradle/deploy.gradle +++ /dev/null @@ -1,148 +0,0 @@ -apply plugin: 'maven-publish' -apply plugin: 'com.jfrog.bintray' - -Properties properties = new Properties() -File projectPropertiesFile = rootProject.file("gradle/deploy.properties") -if (projectPropertiesFile.exists()) { - properties.load(projectPropertiesFile.newDataInputStream()) -} else { - throw new Error("Cannot find deploy.properties file in gradle folder") -} - -def isAndroidLib = project.getPlugins().hasPlugin('com.android.application') || - project.getPlugins().hasPlugin('com.android.library') - -if(isAndroidLib){ - task androidSourcesJar(type: Jar) { - from android.sourceSets.main.java.srcDirs - classifier = 'sources' - } - task androidJavadoc(type: Javadoc) { - failOnError false - source = android.sourceSets.main.java.srcDirs - classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) - } - - task androidJavadocJar(type: Jar, dependsOn: androidJavadoc) { - classifier = 'javadoc' - from androidJavadoc.destinationDir - } -}else{ - task sourcesJar(type: Jar, dependsOn:classes) { - classifier = 'sources' - from sourceSets.main.allSource - } - - task javadocJar(type: Jar, dependsOn:javadoc) { - classifier = 'javadoc' - from javadoc.destinationDir - } -} - -def pomConfig = { - licenses { - license { - name properties.getProperty("license.name") - url properties.getProperty("license.url") - } - } - developers { - developer { - id properties.getProperty("developer.id") - name properties.getProperty("developer.name") - email properties.getProperty("developer.email") - } - } - scm { - connection POM_GIT_URL - developerConnection POM_GIT_URL - url POM_SITE_URL - } -} - -publishing { - publications { - mavenjava(MavenPublication) { - groupId project.group - version project.version - if(isAndroidLib){ - artifact file("$buildDir/outputs/aar/${project.name}-release.aar") - artifact androidSourcesJar - artifact androidJavadocJar - pom.withXml { - def root = asNode() - final dependenciesNode = root.appendNode('dependencies') - - ext.addDependency = { dep, String scope -> - if (dep.group == null || dep.version == null || dep.name == null || dep.name == "unspecified") - return // ignore invalid dependencies - - final dependencyNode = dependenciesNode.appendNode('dependency') - dependencyNode.appendNode('groupId', dep.group) - dependencyNode.appendNode('artifactId', dep.name) - dependencyNode.appendNode('version', dep.version) - dependencyNode.appendNode('scope', scope) - - if (!dep.transitive) { - // If this dependency is not transitive, we should force exclude all its dependencies from the POM - final exclusionNode = dependencyNode.appendNode('exclusions').appendNode('exclusion') - exclusionNode.appendNode('groupId', '*') - exclusionNode.appendNode('artifactId', '*') - } else if (!dep.properties.excludeRules.empty) { - // Otherwise add specified exclude rules - final exclusionsNode = dependencyNode.appendNode('exclusions') - dep.properties.excludeRules.each { rule -> - final exclusionNode = exclusionsNode.appendNode('exclusion') - exclusionNode.appendNode('groupId', rule.group ?: '*') - exclusionNode.appendNode('artifactId', rule.module ?: '*') - } - } - } - - // List all "compile" dependencies (for old Gradle) - configurations.compile.getDependencies().each { dep -> addDependency(dep, "compile") } - // List all "implementation" dependencies (for new Gradle) as "runtime" dependencies - configurations.implementation.getDependencies().each { dep -> addDependency(dep, "runtime") } - // List all "api" dependencies (for new Gradle) as "compile" dependencies - configurations.api.getDependencies().each { dep -> addDependency(dep, "compile") } - - root.children().last() + pomConfig - } - }else{ - from components.java - artifact sourcesJar - artifact javadocJar - - pom.withXml { - def root = asNode() - root.children().last() + pomConfig - } - } - - } - } - repositories { - maven { - url properties.getProperty("maven.url") - credentials { - username properties.getProperty("maven.username") - password properties.getProperty("maven.password") - } - } - } -} - -bintray { - user = properties.getProperty("bintray.user") - key = properties.getProperty("bintray.apikey") - publications = ['mavenjava'] - pkg { - repo = 'qmuirepo' - name = project.name - websiteUrl =POM_SITE_URL - vcsUrl = POM_GIT_URL - licenses = ["MIT"] - publish = true - } -} - diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e09b7dc50..b39115014 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Mar 30 17:42:06 CST 2020 +#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-5.6.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip diff --git a/lib/build.gradle b/lib/build.gradle deleted file mode 100644 index b2b6bbae7..000000000 --- a/lib/build.gradle +++ /dev/null @@ -1,8 +0,0 @@ -apply plugin: 'java-library' - -dependencies { - compile fileTree(dir: 'libs', include: ['*.jar']) -} - -sourceCompatibility = "1.7" -targetCompatibility = "1.7" diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts new file mode 100644 index 000000000..c0a769977 --- /dev/null +++ b/lib/build.gradle.kts @@ -0,0 +1,10 @@ +import com.qmuiteam.plugin.Dep + +plugins { + `java-library` +} + +java { + sourceCompatibility = Dep.javaVersion + targetCompatibility = Dep.javaVersion +} diff --git a/lint/.gitignore b/lint/.gitignore deleted file mode 100644 index 64ad06255..000000000 --- a/lint/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -/*.bin -/*.iml -/.DS_Store -/.gradletasknamecache -/.idea -/bin -/build -/local.properties -/deploy.properties diff --git a/lint/build.gradle b/lint/build.gradle deleted file mode 100644 index 31af1bf3d..000000000 --- a/lint/build.gradle +++ /dev/null @@ -1,20 +0,0 @@ -apply plugin: 'com.android.library' - -version = QMUI_LINT_VERSION - -android { - compileSdkVersion project.ext.compileSdkVersion - - defaultConfig { - minSdkVersion 14 - } -} - -dependencies { - lintPublish project(':lintrule') -} - -File deployConfig = rootProject.file('gradle/deploy.properties') -if (deployConfig.exists()) { - apply from: rootProject.file('gradle/deploy.gradle') -} diff --git a/lint/src/main/AndroidManifest.xml b/lint/src/main/AndroidManifest.xml deleted file mode 100644 index 4307d152a..000000000 --- a/lint/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ -<manifest - package="com.qmuiteam.qmui.lint"> -</manifest> diff --git a/lintrule/.gitignore b/lintrule/.gitignore deleted file mode 100644 index d8006e9de..000000000 --- a/lintrule/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -.idea -.DS_Store -local.properties -/*.iml -/build diff --git a/lintrule/build.gradle b/lintrule/build.gradle deleted file mode 100644 index 691153d19..000000000 --- a/lintrule/build.gradle +++ /dev/null @@ -1,16 +0,0 @@ -apply plugin: 'java-library' - -dependencies { - def lintVersion = '26.4.2' - compileOnly "com.android.tools.lint:lint-api:$lintVersion" - compileOnly "com.android.tools.lint:lint-checks:$lintVersion" -} - -sourceCompatibility = "1.7" -targetCompatibility = "1.7" - -jar { - manifest { - attributes("Lint-Registry-v2": 'com.qmuiteam.qmui.lint.QMUIIssueRegistry') - } -} diff --git a/lintrule/src/main/java/com/qmuiteam/qmui/lint/QMUIFWordDetector.java b/lintrule/src/main/java/com/qmuiteam/qmui/lint/QMUIFWordDetector.java deleted file mode 100644 index 8cad580cd..000000000 --- a/lintrule/src/main/java/com/qmuiteam/qmui/lint/QMUIFWordDetector.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, - * either express or implied. See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.qmuiteam.qmui.lint; - - -import com.android.tools.lint.client.api.JavaEvaluator; -import com.android.tools.lint.client.api.UElementHandler; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Detector; -import com.android.tools.lint.detector.api.Implementation; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.JavaContext; -import com.android.tools.lint.detector.api.Scope; -import com.android.tools.lint.detector.api.Severity; -import com.google.common.collect.Lists; -import com.intellij.psi.PsiMethod; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.uast.UCallExpression; -import org.jetbrains.uast.UElement; -import org.jetbrains.uast.UExpression; - -import java.util.Arrays; -import java.util.EnumSet; -import java.util.List; - -/** - * 检测 QMUILog 中是否使用了 F Word。 - * Created by Kayo on 2017/9/19. - */ -public class QMUIFWordDetector extends Detector implements Detector.UastScanner { - public static final Issue ISSUE_F_WORD = - Issue.create("QMUIDontUseTheFWordInLog", - "Please, don't use the f word, type something more nicely.", - "Do I need to explain this? \uD83D\uDD95", - Category.CORRECTNESS, 5, Severity.WARNING, - new Implementation(QMUIFWordDetector.class, EnumSet.of(Scope.JAVA_FILE))); - - @Nullable - @Override - public List<Class<? extends UElement>> getApplicableUastTypes() { - return Lists.<Class<? extends UElement>>newArrayList( - UCallExpression.class); - } - - @Nullable - @Override - public UElementHandler createUastHandler(@NotNull JavaContext context) { - return new FWordHandler(context); - } - - static class FWordHandler extends UElementHandler { - private static final List<String> checkMedthods = Arrays.asList("e", "w", "i", "d"); - private static final List<String> fWords = Arrays.asList("fuck", "bitch", "bullshit"); - - private JavaContext mJavaContext; - - FWordHandler(@NotNull JavaContext context) { - mJavaContext = context; - } - - @Override - public void visitCallExpression(@NotNull UCallExpression node) { - JavaEvaluator evaluator = mJavaContext.getEvaluator(); - PsiMethod method = node.resolve(); - if (evaluator.isMemberInClass(method, "android.util.Log") || - evaluator.isMemberInClass(method, "com.qmuiteam.qmui.QMUILog")) { - String methodName = node.getMethodName(); - if (checkMedthods.contains(methodName)) { - List<UExpression> expressions = node.getValueArguments(); - for (UExpression expression : expressions) { - String text = expression.asRenderString(); - for(String fword: fWords){ - int index = text.indexOf(fword); - if(index >= 0){ - mJavaContext.report( - ISSUE_F_WORD, expression, - mJavaContext.getRangeLocation(expression, index, fword.length()), "\uD83D\uDD95"); - } - } - - } - - } - } - } - } -} diff --git a/lintrule/src/main/java/com/qmuiteam/qmui/lint/QMUIImageScaleDetector.java b/lintrule/src/main/java/com/qmuiteam/qmui/lint/QMUIImageScaleDetector.java deleted file mode 100644 index 27d463638..000000000 --- a/lintrule/src/main/java/com/qmuiteam/qmui/lint/QMUIImageScaleDetector.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, - * either express or implied. See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.qmuiteam.qmui.lint; - -import com.android.resources.ResourceFolderType; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Detector; -import com.android.tools.lint.detector.api.Implementation; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.Location; -import com.android.tools.lint.detector.api.ResourceContext; -import com.android.tools.lint.detector.api.Scope; -import com.android.tools.lint.detector.api.Severity; - -import java.awt.image.BufferedImage; -import java.io.File; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import javax.imageio.ImageIO; - -/** - * 检测图片的尺寸的正确性,例如2倍图的宽高应该为偶数,3倍图的宽高应该为3的倍数 - * Created by Kayo on 2017/9/7. - */ - -public class QMUIImageScaleDetector extends Detector implements Detector.BinaryResourceScanner { - - public static final Issue ISSUE_IMAGE_SCALE = - Issue.create("QMUIImageSizeDisproportionate", - "The size of this image is disproportionate.", - "Please check the size of the image, for example, the height and width of the 3x plot should be 1.5 times 2x plot.", - Category.ICONS, 4, Severity.WARNING, - new Implementation(QMUIImageScaleDetector.class, Scope.BINARY_RESOURCE_FILE_SCOPE)); - - private static final String IGNORE_IMAGE_NIGHT_PNG = ".9.png"; - private static final String CHECK_IMAGE_WEBP = ".webp"; - private static final String CHECK_IMAGE_PNG = ".png"; - private static final String CHECK_IMAGE_JPEG = ".jpeg"; - private static final String CHECK_IMAGE_JPG = ".jpg"; - - @Override - public boolean appliesTo(ResourceFolderType var1) { - return var1.getName().equalsIgnoreCase(String.valueOf(ResourceFolderType.MIPMAP)) || var1.getName().equalsIgnoreCase(String.valueOf(ResourceFolderType.DRAWABLE)); - } - - @Override - public void checkBinaryResource(ResourceContext context) { - - String filename = context.file.getName(); - - if (filename.contains(IGNORE_IMAGE_NIGHT_PNG)) { - return; - } - - if (filename.contains(CHECK_IMAGE_WEBP) || filename.contains(CHECK_IMAGE_PNG) || filename.contains(CHECK_IMAGE_JPEG) || filename.contains(CHECK_IMAGE_JPG)) { - String filePath = context.file.getPath(); - String pattern = ".*?[mipmap|drawable]\\-xhdpi.*?"; - Pattern r = Pattern.compile(pattern); - Matcher m = r.matcher(filePath); - if (m.find()) { - String threePlotFilePath = filePath.replace("xhdpi", "xxhdpi"); - File threePlotFile = new File(threePlotFilePath); - try { - BufferedImage targetImage = ImageIO.read(context.file); - int targetWidth = targetImage.getWidth(); - int targetHeight = targetImage.getHeight(); - - BufferedImage threePlotImage = ImageIO.read(threePlotFile); - int threePlotWidth = threePlotImage.getWidth(); - int threePlotHeight = threePlotImage.getHeight(); - if ((double) threePlotWidth / targetWidth != 1.5 || (double) threePlotHeight / targetHeight != 1.5) { - Location fileLocation = Location.create(context.file); - context.report(ISSUE_IMAGE_SCALE, fileLocation, "2倍图 " + filePath + - " 与其3倍图宽高分别为 (" + targetWidth + ", " + targetHeight + ") 和 (" + threePlotWidth + ", " + threePlotHeight + "),不符合比例关系。"); - } - } catch (Exception ignored) { - } - } - } - - } -} diff --git a/lintrule/src/main/java/com/qmuiteam/qmui/lint/QMUIImageSizeDetector.java b/lintrule/src/main/java/com/qmuiteam/qmui/lint/QMUIImageSizeDetector.java deleted file mode 100644 index 121475a80..000000000 --- a/lintrule/src/main/java/com/qmuiteam/qmui/lint/QMUIImageSizeDetector.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, - * either express or implied. See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.qmuiteam.qmui.lint; - -import com.android.resources.ResourceFolderType; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Detector; -import com.android.tools.lint.detector.api.Implementation; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.Location; -import com.android.tools.lint.detector.api.ResourceContext; -import com.android.tools.lint.detector.api.Scope; -import com.android.tools.lint.detector.api.Severity; - -import java.awt.image.BufferedImage; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import javax.imageio.ImageIO; - -/** - * 检测图片的尺寸的正确性,例如2倍图的宽高应该为偶数,3倍图的宽高应该为3的倍数 - * Created by Kayo on 2017/8/30. - */ - -public class QMUIImageSizeDetector extends Detector implements Detector.BinaryResourceScanner { - - public static final Issue ISSUE_IMAGE_SIZE = - Issue.create("QMUIImageSizeInvalid", - "The size of this image is not correct.", - "Please check the size of the image, for example, the height and width of the 2x plot should be even.", - Category.ICONS, 2, Severity.WARNING, - new Implementation(QMUIImageSizeDetector.class, Scope.BINARY_RESOURCE_FILE_SCOPE)); - - private static final String IGNORE_IMAGE_NIGHT_PNG = ".9.png"; - private static final String CHECK_IMAGE_WEBP = ".webp"; - private static final String CHECK_IMAGE_PNG = ".png"; - private static final String CHECK_IMAGE_JPEG = ".jpeg"; - private static final String CHECK_IMAGE_JPG = ".jpg"; - - /** - * 去掉数值多余的0与小数点符号 - */ - public static String trimZeroAndDot(double number) { - String value = String.valueOf(number); - if (value.indexOf(".") > 0) { - value = value.replaceAll("0+?$", ""); // 去掉多余的0 - value = value.replaceAll("[.]$", ""); // 若此时最后一位是小数点符号,则去掉该符号 - } - return value; - } - - @Override - public boolean appliesTo(ResourceFolderType var1) { - return var1.getName().equalsIgnoreCase(String.valueOf(ResourceFolderType.MIPMAP)) || var1.getName().equalsIgnoreCase(String.valueOf(ResourceFolderType.DRAWABLE)); - } - - @Override - public void checkBinaryResource(ResourceContext context) { - - String filename = context.file.getName(); - - if (filename.contains(IGNORE_IMAGE_NIGHT_PNG)) { - return; - } - - if (filename.contains(CHECK_IMAGE_WEBP) || filename.contains(CHECK_IMAGE_PNG) || filename.contains(CHECK_IMAGE_JPEG) || filename.contains(CHECK_IMAGE_JPG)) { - String filePath = context.file.getPath(); - String pattern = ".*?[mipmap|drawable]\\-(x*)hdpi.*?"; - Pattern r = Pattern.compile(pattern); - Matcher m = r.matcher(filePath); - if (m.find()) { - double multiple = 1.5; - if (m.group(1).length() > 0) { - multiple = m.group(1).length() + 1; - } - try { - BufferedImage targetImage = ImageIO.read(context.file); - int width = targetImage.getWidth(); - int height = targetImage.getHeight(); - if (width % multiple != 0 || height % multiple != 0) { - Location fileLocation = Location.create(context.file); - context.report(ISSUE_IMAGE_SIZE, fileLocation, filePath + " 为" + trimZeroAndDot(multiple) + "倍图,其宽高应该是" + trimZeroAndDot(multiple) + "的倍数,目前宽高为 (" + width + ", " + height + ")。"); - } - } catch (Exception ignored) { - } - } - } - - } -} diff --git a/lintrule/src/main/java/com/qmuiteam/qmui/lint/QMUIIssueRegistry.java b/lintrule/src/main/java/com/qmuiteam/qmui/lint/QMUIIssueRegistry.java deleted file mode 100644 index e7fe548b3..000000000 --- a/lintrule/src/main/java/com/qmuiteam/qmui/lint/QMUIIssueRegistry.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, - * either express or implied. See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.qmuiteam.qmui.lint; - -import com.android.tools.lint.client.api.IssueRegistry; -import com.android.tools.lint.detector.api.Issue; - -import java.util.Arrays; -import java.util.List; - -import static com.android.tools.lint.detector.api.ApiKt.CURRENT_API; - -@SuppressWarnings("unused") -public final class QMUIIssueRegistry extends IssueRegistry { - @Override public List<Issue> getIssues() { - return Arrays.asList( - QMUIFWordDetector.ISSUE_F_WORD, - QMUIJavaVectorDrawableDetector.ISSUE_JAVA_VECTOR_DRAWABLE, - QMUIXmlVectorDrawableDetector.ISSUE_XML_VECTOR_DRAWABLE, - QMUIImageSizeDetector.ISSUE_IMAGE_SIZE, - QMUIImageScaleDetector.ISSUE_IMAGE_SCALE - ); - } - - @Override - public int getApi() { - return CURRENT_API; - } -} diff --git a/lintrule/src/main/java/com/qmuiteam/qmui/lint/QMUIJavaVectorDrawableDetector.java b/lintrule/src/main/java/com/qmuiteam/qmui/lint/QMUIJavaVectorDrawableDetector.java deleted file mode 100644 index 0896d0ca6..000000000 --- a/lintrule/src/main/java/com/qmuiteam/qmui/lint/QMUIJavaVectorDrawableDetector.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, - * either express or implied. See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.qmuiteam.qmui.lint; - -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Detector; -import com.android.tools.lint.detector.api.Implementation; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.JavaContext; -import com.android.tools.lint.detector.api.Project; -import com.android.tools.lint.detector.api.Scope; -import com.android.tools.lint.detector.api.Severity; -import com.intellij.psi.JavaElementVisitor; -import com.intellij.psi.PsiExpression; -import com.intellij.psi.PsiExpressionList; -import com.intellij.psi.PsiMethod; -import com.intellij.psi.PsiMethodCallExpression; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.InputStreamReader; -import java.util.Collections; -import java.util.List; - -/** - * 检测是否在 getDrawable 方法中传入了 Vector Drawable,在 4.0 及以下版本的系统中会导致 Crash - * Created by Kayo on 2017/8/24. - */ - -public class QMUIJavaVectorDrawableDetector extends Detector implements Detector.UastScanner { - - public static final Issue ISSUE_JAVA_VECTOR_DRAWABLE = - Issue.create("QMUIGetVectorDrawableWithWrongFunction", - "Should use the corresponding method to get vector drawable.", - "Using the normal method to get the vector drawable will cause a crash on Android versions below 4.0", - Category.CORRECTNESS, 8, Severity.ERROR, - new Implementation(QMUIJavaVectorDrawableDetector.class, Scope.JAVA_FILE_SCOPE)); - - @Override - public List<String> getApplicableMethodNames() { - return Collections.singletonList("getDrawable"); - } - - @Override - public void visitMethod(@NotNull JavaContext context, - @Nullable JavaElementVisitor visitor, - @NotNull PsiMethodCallExpression call, - @NotNull PsiMethod method) { - super.visitMethod(context, visitor, call, method); - PsiExpressionList args = call.getArgumentList(); - if (args.getExpressions().length == 0) { - return; - } - - Project project = context.getProject(); - List<File> resourceFolder = project.getResourceFolders(); - if (resourceFolder.isEmpty()) { - return; - } - - String resourcePath = resourceFolder.get(0).getAbsolutePath(); - for (PsiExpression expression : args.getExpressions()) { - String input = expression.toString(); - if (input != null && input.contains("R.drawable")) { - // 找出 drawable 相关的参数 - - // 获取 drawable 名字 - String drawableName = input.replace("R.drawable.", ""); - try { - // 若 drawable 为 Vector Drawable,则文件后缀为 xml,根据 resource 路径,drawable 名字,文件后缀拼接出完整路径 - FileInputStream fileInputStream = new FileInputStream(resourcePath + "/drawable/" + drawableName + ".xml"); - BufferedReader reader = new BufferedReader(new InputStreamReader(fileInputStream)); - String line = reader.readLine(); - if (line.contains("vector")) { - // 若文件存在,并且包含首行包含 vector,则为 Vector Drawable,抛出警告 - context.report(ISSUE_JAVA_VECTOR_DRAWABLE, method, context.getLocation(method), expression.toString() + " 为 Vector Drawable,请使用 getVectorDrawable 方法获取,避免 4.0 及以下版本的系统产生 Crash"); - } - reader.close(); - fileInputStream.close(); - } catch (Exception ignored) { - } - } - } - } -} diff --git a/lintrule/src/main/java/com/qmuiteam/qmui/lint/QMUIXmlVectorDrawableDetector.java b/lintrule/src/main/java/com/qmuiteam/qmui/lint/QMUIXmlVectorDrawableDetector.java deleted file mode 100644 index 5cddfebfa..000000000 --- a/lintrule/src/main/java/com/qmuiteam/qmui/lint/QMUIXmlVectorDrawableDetector.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, - * either express or implied. See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.qmuiteam.qmui.lint; - -import com.android.annotations.NonNull; -import com.android.resources.ResourceFolderType; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Implementation; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.Project; -import com.android.tools.lint.detector.api.ResourceXmlDetector; -import com.android.tools.lint.detector.api.Scope; -import com.android.tools.lint.detector.api.Severity; -import com.android.tools.lint.detector.api.XmlContext; -import com.google.common.collect.Lists; - -import org.w3c.dom.Attr; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.util.Collection; -import java.util.List; - -import static com.android.SdkConstants.ATTR_DRAWABLE_BOTTOM; -import static com.android.SdkConstants.ATTR_DRAWABLE_LEFT; -import static com.android.SdkConstants.ATTR_DRAWABLE_RIGHT; -import static com.android.SdkConstants.ATTR_DRAWABLE_TOP; - -/** - * 检测是否在 drawableLeft / drawableRight / drawableTop / drawableBottom 中传入了 Vector Drawable,在 4.0 及以下版本的系统中会导致 Crash - * Created by Kayo on 2017/8/29. - */ - -public class QMUIXmlVectorDrawableDetector extends ResourceXmlDetector { - - public static final Issue ISSUE_XML_VECTOR_DRAWABLE = - Issue.create("QMUIGetVectorDrawableWithWrongProperty", - "Should use the corresponding property to get vector drawable.", - "Using the normal property to get the vector drawable will cause a crash on Android versions below 4.0.", - Category.CORRECTNESS, 8, Severity.ERROR, - new Implementation(QMUIXmlVectorDrawableDetector.class, Scope.RESOURCE_FILE_SCOPE)); - - private static final Collection<String> mAttrList = Lists.newArrayList(ATTR_DRAWABLE_LEFT, ATTR_DRAWABLE_RIGHT, ATTR_DRAWABLE_TOP, ATTR_DRAWABLE_BOTTOM); - - @Override - public boolean appliesTo(ResourceFolderType folderType) { - return ResourceFolderType.LAYOUT == folderType; - } - - @Override - public Collection<String> getApplicableElements() { - return ALL; - } - - @Override - public Collection<String> getApplicableAttributes() { - return mAttrList; - } - - @Override - public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) { - // 判断资源文件夹是否存在 - Project project = context.getProject(); - List<File> resourceFolder = project.getResourceFolders(); - if (resourceFolder.isEmpty()) { - return; - } - - // 获取项目资源文件夹路径 - String resourcePath = resourceFolder.get(0).getAbsolutePath(); - // 获取 drawable 名字 - String drawableName = attribute.getValue().replace("@drawable/", ""); - FileInputStream fileInputStream = null; - BufferedReader reader = null; - try { - // 若 drawable 为 Vector Drawable,则文件后缀为 xml,根据 resource 路径,drawable 名字,文件后缀拼接出完整路径 - fileInputStream = new FileInputStream(resourcePath + "/drawable/" + drawableName + ".xml"); - reader = new BufferedReader(new InputStreamReader(fileInputStream)); - String line = reader.readLine(); - if (line.contains("vector")) { - // 若文件存在,并且包含首行包含 vector,则为 Vector Drawable,抛出警告 - context.report(ISSUE_XML_VECTOR_DRAWABLE, attribute, context.getLocation(attribute), attribute.getValue() + " 为 Vector Drawable,请使用 Vector 属性进行设置,避免 4.0 及以下版本的系统产生 Crash"); - } - } catch (Exception ignored) { - - }finally { - if(fileInputStream != null){ - try { - fileInputStream.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - if(reader != null){ - try { - reader.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - } - } -} diff --git a/photo-coil/.gitignore b/photo-coil/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/photo-coil/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/photo-coil/build.gradle.kts b/photo-coil/build.gradle.kts new file mode 100644 index 000000000..c8f41852c --- /dev/null +++ b/photo-coil/build.gradle.kts @@ -0,0 +1,52 @@ +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) +} \ No newline at end of file diff --git a/qmuidemo-skin-code-generator-source b/photo-coil/consumer-rules.pro similarity index 100% rename from qmuidemo-skin-code-generator-source rename to photo-coil/consumer-rules.pro diff --git a/photo-coil/proguard-rules.pro b/photo-coil/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/photo-coil/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/photo-coil/src/androidTest/java/com/qmuiteam/ExampleInstrumentedTest.kt b/photo-coil/src/androidTest/java/com/qmuiteam/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..d8cbc95f6 --- /dev/null +++ b/photo-coil/src/androidTest/java/com/qmuiteam/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +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) + } +} \ No newline at end of file diff --git a/photo-coil/src/main/AndroidManifest.xml b/photo-coil/src/main/AndroidManifest.xml new file mode 100644 index 000000000..15e0757af --- /dev/null +++ b/photo-coil/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.qmuiteam.photo.coil"> + +</manifest> \ No newline at end of file diff --git a/photo-coil/src/main/java/com/qmuiteam/photo/coil/QMUICoilImageDecoderFactory.kt b/photo-coil/src/main/java/com/qmuiteam/photo/coil/QMUICoilImageDecoderFactory.kt new file mode 100644 index 000000000..a4b4cb052 --- /dev/null +++ b/photo-coil/src/main/java/com/qmuiteam/photo/coil/QMUICoilImageDecoderFactory.kt @@ -0,0 +1,81 @@ +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 + ) + } + } +} \ No newline at end of file diff --git a/photo-coil/src/main/java/com/qmuiteam/photo/coil/QMUICoilPhoto.kt b/photo-coil/src/main/java/com/qmuiteam/photo/coil/QMUICoilPhoto.kt new file mode 100644 index 000000000..e7a024c9d --- /dev/null +++ b/photo-coil/src/main/java/com/qmuiteam/photo/coil/QMUICoilPhoto.kt @@ -0,0 +1,304 @@ +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<Bitmap?>(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<QMUIBitmapRegionProvider>()) + } + 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<out PhotoTransitionProviderRecover>? { + return QMUICoilPhotoTransitionProviderRecover::class.java + } +} + +class QMUICoilPhotoTransitionProviderRecover : PhotoTransitionProviderRecover { + override fun recover(bundle: Bundle): QMUIPhotoTransitionInfo? { + val uri = bundle.getParcelable<Uri>(QMUICoilPhotoProvider.META_URI_KEY) ?: return null + val thumbUri = bundle.getParcelable<Uri>(QMUICoilPhotoProvider.META_THUMB_URI_KEY) ?: uri + val ratio = bundle.getFloat(QMUICoilPhotoProvider.META_RATIO_KEY) + return QMUIPhotoTransitionInfo( + QMUICoilPhotoProvider(uri, thumbUri, ratio), + null, + null, + null + ) + } +} \ No newline at end of file diff --git a/photo-coil/src/main/java/com/qmuiteam/photo/coil/QMUIMediaCoilPhotoProviderFactory.kt b/photo-coil/src/main/java/com/qmuiteam/photo/coil/QMUIMediaCoilPhotoProviderFactory.kt new file mode 100644 index 000000000..4bb04b4d9 --- /dev/null +++ b/photo-coil/src/main/java/com/qmuiteam/photo/coil/QMUIMediaCoilPhotoProviderFactory.kt @@ -0,0 +1,15 @@ +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() + ) + } +} \ No newline at end of file diff --git a/photo-coil/src/test/java/com/qmuiteam/ExampleUnitTest.kt b/photo-coil/src/test/java/com/qmuiteam/ExampleUnitTest.kt new file mode 100644 index 000000000..9031c50a5 --- /dev/null +++ b/photo-coil/src/test/java/com/qmuiteam/ExampleUnitTest.kt @@ -0,0 +1,10 @@ +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 { + +} \ No newline at end of file diff --git a/photo-glide/.gitignore b/photo-glide/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/photo-glide/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/photo-glide/build.gradle.kts b/photo-glide/build.gradle.kts new file mode 100644 index 000000000..17614acce --- /dev/null +++ b/photo-glide/build.gradle.kts @@ -0,0 +1,54 @@ +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) +} \ No newline at end of file diff --git a/photo-glide/consumer-rules.pro b/photo-glide/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/photo-glide/proguard-rules.pro b/photo-glide/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/photo-glide/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/photo-glide/src/androidTest/java/com/qmuiteam/ExampleInstrumentedTest.kt b/photo-glide/src/androidTest/java/com/qmuiteam/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..d8cbc95f6 --- /dev/null +++ b/photo-glide/src/androidTest/java/com/qmuiteam/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +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) + } +} \ No newline at end of file diff --git a/photo-glide/src/main/AndroidManifest.xml b/photo-glide/src/main/AndroidManifest.xml new file mode 100644 index 000000000..12e41d15b --- /dev/null +++ b/photo-glide/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.qmuiteam.photo.glide"> + +</manifest> \ No newline at end of file diff --git a/photo-glide/src/main/java/com/qmuiteam/photo/glide/QMUIGlideModule.kt b/photo-glide/src/main/java/com/qmuiteam/photo/glide/QMUIGlideModule.kt new file mode 100644 index 000000000..93d0a490b --- /dev/null +++ b/photo-glide/src/main/java/com/qmuiteam/photo/glide/QMUIGlideModule.kt @@ -0,0 +1,102 @@ +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<InputStream, QMUILongGlidePhotoData> { + override fun handles(source: InputStream, options: Options): Boolean { + return true + } + + override fun decode(source: InputStream, width: Int, height: Int, options: Options): Resource<QMUILongGlidePhotoData> { + return doDecode(context, source, width, height, options) + } + + }) + } +} + +private fun doDecode( + context: Context, + source: InputStream, + width: Int, + height: Int, + options: Options +): Resource<QMUILongGlidePhotoData> { + 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 + } +} \ No newline at end of file diff --git a/photo-glide/src/main/java/com/qmuiteam/photo/glide/QMUIGlidePhoto.kt b/photo-glide/src/main/java/com/qmuiteam/photo/glide/QMUIGlidePhoto.kt new file mode 100644 index 000000000..82e3a49a9 --- /dev/null +++ b/photo-glide/src/main/java/com/qmuiteam/photo/glide/QMUIGlidePhoto.kt @@ -0,0 +1,280 @@ +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<Pair<Long, Drawable?>?>(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<QMUILongGlidePhotoData>( + constraints.maxWidth, + constraints.maxHeight + ) { + + override fun onResourceReady(resource: QMUILongGlidePhotoData, transition: Transition<in QMUILongGlidePhotoData>?) { + 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<Drawable>( + constraints.maxWidth, + constraints.maxHeight + ) { + + override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) { + 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<out PhotoTransitionProviderRecover>? { + return QMUIGlidePhotoTransitionProviderRecover::class.java + } +} + +class QMUIGlidePhotoTransitionProviderRecover : PhotoTransitionProviderRecover { + override fun recover(bundle: Bundle): QMUIPhotoTransitionInfo? { + val uri = bundle.getParcelable<Uri>(QMUIGlidePhotoProvider.META_URI_KEY) ?: return null + val thumbUri = bundle.getParcelable<Uri>(QMUIGlidePhotoProvider.META_THUMB_URI_KEY) ?: uri + val ratio = bundle.getFloat(QMUIGlidePhotoProvider.META_RATIO_KEY) + return QMUIPhotoTransitionInfo( + QMUIGlidePhotoProvider(uri, thumbUri, ratio), + null, + null, + null + ) + } +} \ No newline at end of file diff --git a/photo-glide/src/main/java/com/qmuiteam/photo/glide/QMUIMediaGlidePhotoProviderFactory.kt b/photo-glide/src/main/java/com/qmuiteam/photo/glide/QMUIMediaGlidePhotoProviderFactory.kt new file mode 100644 index 000000000..5c4a32416 --- /dev/null +++ b/photo-glide/src/main/java/com/qmuiteam/photo/glide/QMUIMediaGlidePhotoProviderFactory.kt @@ -0,0 +1,15 @@ +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() + ) + } +} \ No newline at end of file diff --git a/photo-glide/src/test/java/com/qmuiteam/ExampleUnitTest.kt b/photo-glide/src/test/java/com/qmuiteam/ExampleUnitTest.kt new file mode 100644 index 000000000..9031c50a5 --- /dev/null +++ b/photo-glide/src/test/java/com/qmuiteam/ExampleUnitTest.kt @@ -0,0 +1,10 @@ +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 { + +} \ No newline at end of file diff --git a/photo/.gitignore b/photo/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/photo/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/photo/build.gradle.kts b/photo/build.gradle.kts new file mode 100644 index 000000000..7b2c99793 --- /dev/null +++ b/photo/build.gradle.kts @@ -0,0 +1,53 @@ +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) +} \ No newline at end of file diff --git a/photo/consumer-rules.pro b/photo/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/photo/proguard-rules.pro b/photo/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/photo/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/photo/src/androidTest/java/com/qmuiteam/ExampleInstrumentedTest.kt b/photo/src/androidTest/java/com/qmuiteam/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..d8cbc95f6 --- /dev/null +++ b/photo/src/androidTest/java/com/qmuiteam/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +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) + } +} \ No newline at end of file diff --git a/photo/src/main/AndroidManifest.xml b/photo/src/main/AndroidManifest.xml new file mode 100644 index 000000000..b1f7a564f --- /dev/null +++ b/photo/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.qmuiteam.photo"> + +</manifest> \ No newline at end of file diff --git a/photo/src/main/java/com/qmuiteam/photo/activity/QMUIPhotoClipActivity.kt b/photo/src/main/java/com/qmuiteam/photo/activity/QMUIPhotoClipActivity.kt new file mode 100644 index 000000000..933ede300 --- /dev/null +++ b/photo/src/main/java/com/qmuiteam/photo/activity/QMUIPhotoClipActivity.kt @@ -0,0 +1,150 @@ +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<out QMUIPhotoClipActivity>, + 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<Uri>(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) + }) + } + } +} \ No newline at end of file diff --git a/photo/src/main/java/com/qmuiteam/photo/activity/QMUIPhotoPickerActivity.kt b/photo/src/main/java/com/qmuiteam/photo/activity/QMUIPhotoPickerActivity.kt new file mode 100644 index 000000000..cae5df526 --- /dev/null +++ b/photo/src/main/java/com/qmuiteam/photo/activity/QMUIPhotoPickerActivity.kt @@ -0,0 +1,651 @@ +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<QMUIPhotoPickItemInfo> { + override fun createFromParcel(parcel: Parcel): QMUIPhotoPickItemInfo { + return QMUIPhotoPickItemInfo(parcel) + } + + override fun newArray(size: Int): Array<QMUIPhotoPickItemInfo?> { + return arrayOfNulls(size) + } + } + +} + +class QMUIPhotoPickResult(val list: List<QMUIPhotoPickItemInfo>, val isOriginOpen: Boolean) + +fun Intent.getQMUIPhotoPickResult(): QMUIPhotoPickResult? { + val list = getParcelableArrayListExtra<QMUIPhotoPickItemInfo>(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<out QMUIPhotoPickerActivity>, + factoryCls: Class<out QMUIMediaPhotoProviderFactory>, + pickedItems: ArrayList<Uri> = 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<QMUIPhotoPickerViewModel>(factoryProducer = { + object : AbstractSavedStateViewModelFactory(this@QMUIPhotoPickerActivity, intent?.extras) { + override fun <T : ViewModel?> create(key: String, modelClass: Class<T>, handle: SavedStateHandle): T { + val constructor = modelClass.getDeclaredConstructor( + Application::class.java, + SavedStateHandle::class.java, + QMUIMediaDataProvider::class.java, + Array<String>::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<QMUIMediaPhotoBucketVO> + ) { + 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<QMUIMediaPhotoBucketVO>, + pickedItems: List<Long>, + 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<QMUIMediaPhotoBucketVO>, + pickedItems: List<Long> + ) { + val list = remember(scene) { + if (scene.onlySelected) { + viewModel.getPickedVOList() + } else { + data.find { it.id == scene.buckedId }?.list ?: emptyList<QMUIMediaPhotoVO>() + } + } + PhotoPickerPreviewContent(viewModel, list, pickedItems, scene) + } + + @OptIn(ExperimentalPagerApi::class) + @Composable + protected open fun BoxScope.PhotoPickerPreviewContent( + viewModel: QMUIPhotoPickerViewModel, + data: List<QMUIMediaPhotoVO>, + pickedItems: List<Long>, + 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<QMUITopBarItem>(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<QMUIPhotoPickItemInfo>) { + setResult(RESULT_OK, Intent().apply { + putParcelableArrayListExtra(QMUI_PHOTO_RESULT_URI_LIST, arrayListOf<QMUIPhotoPickItemInfo>().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<String> { + return QMUIMediaImagesProvider.DEFAULT_SUPPORT_MIMETYPES + } + + private class SceneHolder<T:QMUIPhotoPickerScene>(var scene: T? = null) +} + + diff --git a/photo/src/main/java/com/qmuiteam/photo/activity/QMUIPhotoViewerActivity.kt b/photo/src/main/java/com/qmuiteam/photo/activity/QMUIPhotoViewerActivity.kt new file mode 100644 index 000000000..1fe1df2b0 --- /dev/null +++ b/photo/src/main/java/com/qmuiteam/photo/activity/QMUIPhotoViewerActivity.kt @@ -0,0 +1,409 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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<out QMUIPhotoViewerActivity>, + list: List<QMUIPhotoTransitionInfo>, + 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<QMUIPhotoViewerViewModel>() + 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<QMUIPhotoTransitionInfo>, index: Int) { + QMUIDefaultPhotoConfigProvider { + PhotoViewer(list, index) + } + } + + @OptIn(ExperimentalPagerApi::class) + @Composable + protected open fun PhotoViewer(list: List<QMUIPhotoTransitionInfo>, 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<Boolean>, + photoTransitionInfo: QMUIPhotoTransitionInfo, + onPhotoLoaded: (PhotoResult) -> Unit + ) { + DefaultPhotoContent(transition, photoTransitionInfo, onPhotoLoaded) + } + + @Composable + protected fun DefaultPhotoContent( + transition: Transition<Boolean>, + 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<Int>(PHOTO_CURRENT_INDEX) ?: 0 + val data: PhotoViewerData? + + private val transitionDeliverKey = state.get<Long>(PHOTO_TRANSITION_DELIVERY_KEY) ?: -1 + + init { + val transitionDeliverData = QMUIPhotoTransitionDelivery.getAndRemove(transitionDeliverKey) + data = if (transitionDeliverData != null) { + transitionDeliverData + } else { + val count = state.get<Int>(PHOTO_COUNT) ?: 0 + if (count > 0) { + val list = arrayListOf<QMUIPhotoTransitionInfo>() + for (i in 0 until count) { + try { + val meta = state.get<Bundle>("${PHOTO_META_KEY_PREFIX}${i}") + val clsName = + state.get<String>("${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) diff --git a/photo/src/main/java/com/qmuiteam/photo/compose/BitmapRegion.kt b/photo/src/main/java/com/qmuiteam/photo/compose/BitmapRegion.kt new file mode 100644 index 000000000..4f57b8e20 --- /dev/null +++ b/photo/src/main/java/com/qmuiteam/photo/compose/BitmapRegion.kt @@ -0,0 +1,39 @@ +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<Bitmap?>(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() + ) + } + } +} \ No newline at end of file diff --git a/photo/src/main/java/com/qmuiteam/photo/compose/GesturePhoto.kt b/photo/src/main/java/com/qmuiteam/photo/compose/GesturePhoto.kt new file mode 100644 index 000000000..391638c92 --- /dev/null +++ b/photo/src/main/java/com/qmuiteam/photo/compose/GesturePhoto.kt @@ -0,0 +1,592 @@ +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<Boolean>, 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<Boolean>, + 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<Boolean>, + 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<Boolean>, + 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<Dp, Dp> { + val layoutRatio = containerWidth / containerHeight + return when { + isLongImage || imageRatio <= 0f -> containerWidth to containerHeight + imageRatio >= layoutRatio -> containerWidth to (containerWidth / imageRatio) + else -> (containerHeight * imageRatio) to containerHeight + } +} \ No newline at end of file diff --git a/photo/src/main/java/com/qmuiteam/photo/compose/Loading.kt b/photo/src/main/java/com/qmuiteam/photo/compose/Loading.kt new file mode 100644 index 000000000..a821964ad --- /dev/null +++ b/photo/src/main/java/com/qmuiteam/photo/compose/Loading.kt @@ -0,0 +1,44 @@ +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 + ) + } + } + } + + } +} \ No newline at end of file diff --git a/photo/src/main/java/com/qmuiteam/photo/compose/PhotoClipper.kt b/photo/src/main/java/com/qmuiteam/photo/compose/PhotoClipper.kt new file mode 100644 index 000000000..d4916c55c --- /dev/null +++ b/photo/src/main/java/com/qmuiteam/photo/compose/PhotoClipper.kt @@ -0,0 +1,165 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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) + } + } +} \ No newline at end of file diff --git a/photo/src/main/java/com/qmuiteam/photo/compose/PhotoConfig.kt b/photo/src/main/java/com/qmuiteam/photo/compose/PhotoConfig.kt new file mode 100644 index 000000000..d10ddb259 --- /dev/null +++ b/photo/src/main/java/com/qmuiteam/photo/compose/PhotoConfig.kt @@ -0,0 +1,38 @@ +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) + ) + } +} \ No newline at end of file diff --git a/photo/src/main/java/com/qmuiteam/photo/compose/Thumbnail.kt b/photo/src/main/java/com/qmuiteam/photo/compose/Thumbnail.kt new file mode 100644 index 000000000..b739be282 --- /dev/null +++ b/photo/src/main/java/com/qmuiteam/photo/compose/Thumbnail.kt @@ -0,0 +1,323 @@ +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<out QMUIPhotoViewerActivity> = QMUIPhotoViewerActivity::class.java, + activity: ComponentActivity, + images: List<QMUIPhotoProvider>, + 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<QMUIPhotoProvider>, + config: QMUIPhotoThumbnailConfig = remember { qmuiDefaultPhotoThumbnailConfig }, + onClick: ((images: List<QMUIPhotoTransitionInfo>, 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<QMUIPhotoProvider>, + renderInfo: Array<QMUIPhotoTransitionInfo>, + config: QMUIPhotoThumbnailConfig, + containerWidth: Dp, + rowCount: Int, + startIndex: Int, + onClick: ((images: List<QMUIPhotoTransitionInfo>, 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 + ) + } + } +} \ No newline at end of file diff --git a/photo/src/main/java/com/qmuiteam/photo/compose/picker/Buckets.kt b/photo/src/main/java/com/qmuiteam/photo/compose/picker/Buckets.kt new file mode 100644 index 000000000..fc16bbdb1 --- /dev/null +++ b/photo/src/main/java/com/qmuiteam/photo/compose/picker/Buckets.kt @@ -0,0 +1,171 @@ +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<QMUIMediaPhotoBucketVO>, + 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 + ) + } +} \ No newline at end of file diff --git a/photo/src/main/java/com/qmuiteam/photo/compose/picker/Common.kt b/photo/src/main/java/com/qmuiteam/photo/compose/picker/Common.kt new file mode 100644 index 000000000..8a777fc9b --- /dev/null +++ b/photo/src/main/java/com/qmuiteam/photo/compose/picker/Common.kt @@ -0,0 +1,275 @@ +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<Boolean>, + 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 + ) + } +} \ No newline at end of file diff --git a/photo/src/main/java/com/qmuiteam/photo/compose/picker/Config.kt b/photo/src/main/java/com/qmuiteam/photo/compose/picker/Config.kt new file mode 100644 index 000000000..3f7ee2bf7 --- /dev/null +++ b/photo/src/main/java/com/qmuiteam/photo/compose/picker/Config.kt @@ -0,0 +1,118 @@ +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<String>, + isFocusFlow: StateFlow<Boolean>, + 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<Int>, + 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<EditPaint> = 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<ColorEditPaint> = 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() + } +} \ No newline at end of file diff --git a/photo/src/main/java/com/qmuiteam/photo/compose/picker/Edit.kt b/photo/src/main/java/com/qmuiteam/photo/compose/picker/Edit.kt new file mode 100644 index 000000000..4d0f04f36 --- /dev/null +++ b/photo/src/main/java/com/qmuiteam/photo/compose/picker/Edit.kt @@ -0,0 +1,872 @@ +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<T : PickerEditScene>(var scene: T? = null) + +private class MutablePickerPhotoInfo( + var drawable: Drawable?, + var mosaicBitmapCache: MutableMap<Int, ImageBitmap> = 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<PickerEditScene>(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<PaintEditLayer>() + } + + val textEditLayers = remember(data) { + mutableStateListOf<TextEditLayer>() + } + + 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<PaintEditLayer>, + 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<PaintEditLayer>, + textEditLayers: List<TextEditLayer>, + 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<PaintEditLayer>, + 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<EditPaint>, + 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<ColorEditPaint>, + 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) + } + } + } + } + +} \ No newline at end of file diff --git a/photo/src/main/java/com/qmuiteam/photo/compose/picker/Grid.kt b/photo/src/main/java/com/qmuiteam/photo/compose/picker/Grid.kt new file mode 100644 index 000000000..95f81ec1e --- /dev/null +++ b/photo/src/main/java/com/qmuiteam/photo/compose/picker/Grid.kt @@ -0,0 +1,217 @@ +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<QMUIMediaPhotoVO>) + +private fun convertToRowData(data: List<QMUIMediaPhotoVO>, rowCount: Int): List<QMUIPhotoPickerGridRowData>{ + val ret = mutableListOf<QMUIPhotoPickerGridRowData>() + var list = mutableListOf<QMUIMediaPhotoVO>() + 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<QMUIMediaPhotoVO>, + modifier: Modifier = Modifier, + state: LazyListState = rememberLazyListState(), + pickedItems: List<Long>, + 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<Long>, + 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<Long>, + 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<Long>, + isOriginOpenFlow: StateFlow<Boolean>, + 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 + ) + } + } +} \ No newline at end of file diff --git a/photo/src/main/java/com/qmuiteam/photo/compose/picker/PaintEdit.kt b/photo/src/main/java/com/qmuiteam/photo/compose/picker/PaintEdit.kt new file mode 100644 index 000000000..574d2b1c8 --- /dev/null +++ b/photo/src/main/java/com/qmuiteam/photo/compose/picker/PaintEdit.kt @@ -0,0 +1,148 @@ +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() { + + } +} \ No newline at end of file diff --git a/photo/src/main/java/com/qmuiteam/photo/compose/picker/Preview.kt b/photo/src/main/java/com/qmuiteam/photo/compose/picker/Preview.kt new file mode 100644 index 000000000..341783392 --- /dev/null +++ b/photo/src/main/java/com/qmuiteam/photo/compose/picker/Preview.kt @@ -0,0 +1,219 @@ +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<QMUIMediaPhotoVO>, + 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<QMUIMediaPhotoVO>, + pickedItems: List<Long>, + 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<Boolean>, + 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 + ) + } +} \ No newline at end of file diff --git a/photo/src/main/java/com/qmuiteam/photo/compose/picker/TextEdit.kt b/photo/src/main/java/com/qmuiteam/photo/compose/picker/TextEdit.kt new file mode 100644 index 000000000..46d2283d0 --- /dev/null +++ b/photo/src/main/java/com/qmuiteam/photo/compose/picker/TextEdit.kt @@ -0,0 +1,426 @@ +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<Offset> = MutableStateFlow(Offset.Zero), + val scaleFlow: MutableStateFlow<Float> = MutableStateFlow(1f), + val rotationFlow: MutableStateFlow<Float> = 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 + } +} \ No newline at end of file diff --git a/photo/src/main/java/com/qmuiteam/photo/compose/picker/TopBarItem.kt b/photo/src/main/java/com/qmuiteam/photo/compose/picker/TopBarItem.kt new file mode 100644 index 000000000..b5085cb73 --- /dev/null +++ b/photo/src/main/java/com/qmuiteam/photo/compose/picker/TopBarItem.kt @@ -0,0 +1,129 @@ +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<String>, + private val isFocusFlow: StateFlow<Boolean>, + 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<Int>, + 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<Boolean> +) { + 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)) + } + } +} \ No newline at end of file diff --git a/photo/src/main/java/com/qmuiteam/photo/data/QMUIBitmapRegion.kt b/photo/src/main/java/com/qmuiteam/photo/data/QMUIBitmapRegion.kt new file mode 100644 index 000000000..4bbf2e362 --- /dev/null +++ b/photo/src/main/java/com/qmuiteam/photo/data/QMUIBitmapRegion.kt @@ -0,0 +1,270 @@ +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<QMUIBitmapRegionProvider>) + + +/** + * 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<QMUIBitmapRegionProvider>() + 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 <T> 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<QMUICacheBitmapRegionLoader, Job>(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 + } +} \ No newline at end of file diff --git a/photo/src/main/java/com/qmuiteam/photo/data/QMUIMediaDataProvider.kt b/photo/src/main/java/com/qmuiteam/photo/data/QMUIMediaDataProvider.kt new file mode 100644 index 000000000..61bb141ed --- /dev/null +++ b/photo/src/main/java/com/qmuiteam/photo/data/QMUIMediaDataProvider.kt @@ -0,0 +1,197 @@ +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<QMUIMediaModel> +) + +class QMUIMediaPhotoBucketVO( + val id: String, + val name: String, + val list: List<QMUIMediaPhotoVO> +) + +class QMUIMediaPhotoVO( + val model: QMUIMediaModel, + val photoProvider: QMUIPhotoProvider +) + +interface QMUIMediaPhotoProviderFactory { + fun factory(model: QMUIMediaModel): QMUIPhotoProvider +} + +interface QMUIMediaDataProvider { + suspend fun provide(context: Context, supportedMimeTypes: Array<String>): List<QMUIMediaPhotoBucket> +} + +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<String>): List<QMUIMediaPhotoBucket> { + 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<QMUIMediaModel>() + 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<MutableMediaPhotoBucket>() + 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<QMUIMediaModel> = mutableListOf() + } + +} + + +private fun <T> 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 diff --git a/photo/src/main/java/com/qmuiteam/photo/data/QMUIPhotoTransitionDelivery.kt b/photo/src/main/java/com/qmuiteam/photo/data/QMUIPhotoTransitionDelivery.kt new file mode 100644 index 000000000..fd22830a2 --- /dev/null +++ b/photo/src/main/java/com/qmuiteam/photo/data/QMUIPhotoTransitionDelivery.kt @@ -0,0 +1,52 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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<Long, PhotoViewerData>() + + @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) + } +} \ No newline at end of file diff --git a/photo/src/main/java/com/qmuiteam/photo/data/QMUIPhotoTransitionInfo.kt b/photo/src/main/java/com/qmuiteam/photo/data/QMUIPhotoTransitionInfo.kt new file mode 100644 index 000000000..2c20cfe1d --- /dev/null +++ b/photo/src/main/java/com/qmuiteam/photo/data/QMUIPhotoTransitionInfo.kt @@ -0,0 +1,126 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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<QMUIPhotoTransitionInfo>, + 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<out PhotoTransitionProviderRecover>? +} + +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<out PhotoTransitionProviderRecover>? { + 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? +) \ No newline at end of file diff --git a/photo/src/main/java/com/qmuiteam/photo/util/BitmapEx.kt b/photo/src/main/java/com/qmuiteam/photo/util/BitmapEx.kt new file mode 100644 index 000000000..c7882eda9 --- /dev/null +++ b/photo/src/main/java/com/qmuiteam/photo/util/BitmapEx.kt @@ -0,0 +1,183 @@ +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() + } +} diff --git a/photo/src/main/java/com/qmuiteam/photo/util/QMUIPhotoHelper.kt b/photo/src/main/java/com/qmuiteam/photo/util/QMUIPhotoHelper.kt new file mode 100644 index 000000000..aa98fa2e9 --- /dev/null +++ b/photo/src/main/java/com/qmuiteam/photo/util/QMUIPhotoHelper.kt @@ -0,0 +1,138 @@ +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 + ) + } +} \ No newline at end of file diff --git a/photo/src/main/java/com/qmuiteam/photo/util/ViewEx.kt b/photo/src/main/java/com/qmuiteam/photo/util/ViewEx.kt new file mode 100644 index 000000000..6f1fb14f8 --- /dev/null +++ b/photo/src/main/java/com/qmuiteam/photo/util/ViewEx.kt @@ -0,0 +1,45 @@ +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) + } +} \ No newline at end of file diff --git a/photo/src/main/java/com/qmuiteam/photo/vm/QMUIPhotoPickerViewModel.kt b/photo/src/main/java/com/qmuiteam/photo/vm/QMUIPhotoPickerViewModel.kt new file mode 100644 index 000000000..2d95fc07f --- /dev/null +++ b/photo/src/main/java/com/qmuiteam/photo/vm/QMUIPhotoPickerViewModel.kt @@ -0,0 +1,179 @@ +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<String> +) : ViewModel(), LogTag { + + val pickLimitCount = state.get<Int>(QMUI_PHOTO_PICK_LIMIT_COUNT) ?: QMUI_PHOTO_DEFAULT_PICK_LIMIT_COUNT + + val enableOrigin = state.get<Boolean>(QMUI_PHOTO_ENABLE_ORIGIN) ?: true + + private val photoProviderFactory: QMUIMediaPhotoProviderFactory + + private val _photoPickerSceneFlow = MutableStateFlow<QMUIPhotoPickerScene>(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<Long, QMUIMediaPhotoVO>() + private val _pickedListFlow = MutableStateFlow<List<Long>>(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<String>(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<ArrayList<Uri>>(QMUI_PHOTO_PICKED_ITEMS) + if(pickedItems != null){ + state.set(QMUI_PHOTO_PICKED_ITEMS, null) + val map = mutableMapOf<Uri, Long>() + _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<Long>() + 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<QMUIMediaPhotoVO>{ + return _pickedListFlow.value.mapNotNull { id -> + _pickedMap[id] + } + } + + fun getPickedResultList(): List<QMUIPhotoPickItemInfo> { + 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<QMUIMediaPhotoBucketVO>?, + val error: Throwable? = null +) \ No newline at end of file diff --git a/photo/src/main/res/anim/scale_enter.xml b/photo/src/main/res/anim/scale_enter.xml new file mode 100644 index 000000000..2eb219429 --- /dev/null +++ b/photo/src/main/res/anim/scale_enter.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Tencent is pleased to support the open source community by making QMUI_Android available. + + Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + + Licensed under the MIT License (the "License"); you may not use this file except in + compliance with the License. You may obtain a copy of the License at + + http://opensource.org/licenses/MIT + + Unless required by applicable law or agreed to in writing, software distributed under the License is + distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + either express or implied. See the License for the specific language governing permissions and + limitations under the License. +--> + +<set xmlns:android="http://schemas.android.com/apk/res/android" + android:background="@android:color/transparent"> + + <scale + android:duration="300" + android:fromXScale="0.9" + android:fromYScale="0.9" + android:interpolator="@android:interpolator/decelerate_cubic" + android:pivotX="50%" + android:pivotY="50%" + android:toXScale="1.0" + android:toYScale="1.0" /> + + <alpha + android:duration="300" + android:fromAlpha="0.0" + android:interpolator="@android:interpolator/decelerate_cubic" + android:toAlpha="1.0" /> + +</set> \ No newline at end of file diff --git a/photo/src/main/res/anim/scale_exit.xml b/photo/src/main/res/anim/scale_exit.xml new file mode 100644 index 000000000..509ea0831 --- /dev/null +++ b/photo/src/main/res/anim/scale_exit.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Tencent is pleased to support the open source community by making QMUI_Android available. + + Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + + Licensed under the MIT License (the "License"); you may not use this file except in + compliance with the License. You may obtain a copy of the License at + + http://opensource.org/licenses/MIT + + Unless required by applicable law or agreed to in writing, software distributed under the License is + distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + either express or implied. See the License for the specific language governing permissions and + limitations under the License. +--> + +<set xmlns:android="http://schemas.android.com/apk/res/android" + android:background="@android:color/transparent"> + + <scale + android:duration="300" + android:fromXScale="1.0" + android:fromYScale="1.0" + android:interpolator="@android:interpolator/decelerate_quad" + android:pivotX="50%" + android:pivotY="50%" + android:toXScale="0.8" + android:toYScale="0.8" /> + + <alpha + android:duration="300" + android:fromAlpha="1.0" + android:interpolator="@android:interpolator/decelerate_quad" + android:toAlpha="0.0" /> + +</set> \ No newline at end of file diff --git a/photo/src/test/java/com/qmuiteam/ExampleUnitTest.kt b/photo/src/test/java/com/qmuiteam/ExampleUnitTest.kt new file mode 100644 index 000000000..9031c50a5 --- /dev/null +++ b/photo/src/test/java/com/qmuiteam/ExampleUnitTest.kt @@ -0,0 +1,10 @@ +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 { + +} \ No newline at end of file diff --git a/plugin/.gitignore b/plugin/.gitignore new file mode 100644 index 000000000..6b3b87822 --- /dev/null +++ b/plugin/.gitignore @@ -0,0 +1,6 @@ +/build +*.iml +.DS_Store +.gradle +.gradletasknamecache +.idea \ No newline at end of file diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts new file mode 100644 index 000000000..f468c8af0 --- /dev/null +++ b/plugin/build.gradle.kts @@ -0,0 +1,62 @@ +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" +} \ No newline at end of file diff --git a/plugin/settings.gradle.kts b/plugin/settings.gradle.kts new file mode 100644 index 000000000..e69de29bb diff --git a/plugin/src/main/java/com/qmuiteam/plugin/Dep.kt b/plugin/src/main/java/com/qmuiteam/plugin/Dep.kt new file mode 100644 index 000000000..fb01e2a9e --- /dev/null +++ b/plugin/src/main/java/com/qmuiteam/plugin/Dep.kt @@ -0,0 +1,86 @@ +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" + } +} \ No newline at end of file diff --git a/plugin/src/main/java/com/qmuiteam/plugin/QMUIDepPlugin.kt b/plugin/src/main/java/com/qmuiteam/plugin/QMUIDepPlugin.kt new file mode 100644 index 000000000..90c982783 --- /dev/null +++ b/plugin/src/main/java/com/qmuiteam/plugin/QMUIDepPlugin.kt @@ -0,0 +1,10 @@ +package com.qmuiteam.plugin + +import org.gradle.api.Plugin +import org.gradle.api.Project + +class QMUIDepPlugin: Plugin<Project>{ + override fun apply(project: Project) { + + } +} \ No newline at end of file diff --git a/plugin/src/main/java/com/qmuiteam/plugin/QMUIPublish.kt b/plugin/src/main/java/com/qmuiteam/plugin/QMUIPublish.kt new file mode 100644 index 000000000..7aa47f11c --- /dev/null +++ b/plugin/src/main/java/com/qmuiteam/plugin/QMUIPublish.kt @@ -0,0 +1,109 @@ +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<Project> { + 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<JavaPluginExtension> { + 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<PublishingExtension> { + repositories { + maven { + setUrl(mavenUrl) + credentials { + username = mavenUsername + password = mavenPassword + } + } + } + publications { + create<MavenPublication>("release") { + + project.configure<SigningExtension> { + 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") +} \ No newline at end of file diff --git a/qmui/build.gradle b/qmui/build.gradle deleted file mode 100644 index 7e49df25c..000000000 --- a/qmui/build.gradle +++ /dev/null @@ -1,66 +0,0 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' - -version = QMUI_VERSION - -//noinspection GroovyMissingReturnStatement -android { - compileSdkVersion parent.ext.compileSdkVersion - lintOptions { - abortOnError false - } - - defaultConfig { - minSdkVersion parent.ext.minSdkVersion - targetSdkVersion parent.ext.targetSdkVersion -// vectorDrawables.useSupportLibrary = true // 与 com.android.support:support-vector-drawable 搭配使用,禁掉 Android Studio 自动生成 png 的功能 - } - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } - -// libraryVariants.all{ variant -> -// variant.mergeResources.doLast { -// replaceTheme variant -// } -// } -// testVariants.all { variant -> -// variant.mergeResources.doLast { -// replaceTheme variant -// } -// } -} - -//def replaceTheme(variant){ -// println "dirName::${variant.dirName}" -// def output = "AppConfigTheme" -// -// File valuesFile = file("${buildDir}/intermediates/res/merged/${variant.dirName}/values/values.xml") -// String content = valuesFile.getText('UTF-8') -// content = content.replaceAll(/\$\{QMUI_PARENT_THEME\}/, output) -// valuesFile.write(content, 'UTF-8') -//} - -dependencies { - api "androidx.appcompat:appcompat:$appcompatVersion" - api "androidx.annotation:annotation:$annotationVersion" - api "com.google.android.material:material:$materialVersion" - api "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion" - api project(':type') - lintChecks project(':lintrule') - - //test - testImplementation "junit:junit:$junitVersion" - api 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0' - compileOnly "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" -} - -// deploy -File deployConfig = rootProject.file('gradle/deploy.properties') -if (deployConfig.exists()) { - apply from: rootProject.file('gradle/deploy.gradle') -} \ No newline at end of file diff --git a/qmui/build.gradle.kts b/qmui/build.gradle.kts new file mode 100644 index 000000000..ee94c5519 --- /dev/null +++ b/qmui/build.gradle.kts @@ -0,0 +1,45 @@ +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) +} \ No newline at end of file diff --git a/qmui/src/main/assets/QMUIWebviewBridge.js b/qmui/src/main/assets/QMUIWebviewBridge.js index 5d4460be4..245e57f8d 100644 --- a/qmui/src/main/assets/QMUIWebviewBridge.js +++ b/qmui/src/main/assets/QMUIWebviewBridge.js @@ -34,25 +34,56 @@ 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(responseStr){ - var response = JSON.parse(responseStr); - if(response.id){ - var responseCallback = responseCallbacks[response.id]; + function _handleResponseFromNative(response){ + if(response && response.callbackId){ + var responseCallback = responseCallbacks[response.callbackId]; if(responseCallback){ responseCallback(response.data); - delete responseCallbacks[response.id]; + delete responseCallbacks[response.callbackId]; } } } var QMUIBridge = window.QMUIBridge = { send: send, + isCmdSupport: isCmdSupport, + getSupportedCmdList: getSupportedCmdList, _fetchQueueFromNative: _fetchQueueFromNative, _handleResponseFromNative: _handleResponseFromNative }; diff --git a/qmui/src/main/java/com/qmuiteam/qmui/exposure/Exposure.kt b/qmui/src/main/java/com/qmuiteam/qmui/exposure/Exposure.kt new file mode 100644 index 000000000..fb9ce2ec1 --- /dev/null +++ b/qmui/src/main/java/com/qmuiteam/qmui/exposure/Exposure.kt @@ -0,0 +1,25 @@ +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) + } +} \ No newline at end of file diff --git a/qmui/src/main/java/com/qmuiteam/qmui/exposure/ExposureChecker.kt b/qmui/src/main/java/com/qmuiteam/qmui/exposure/ExposureChecker.kt new file mode 100644 index 000000000..229f717ec --- /dev/null +++ b/qmui/src/main/java/com/qmuiteam/qmui/exposure/ExposureChecker.kt @@ -0,0 +1,114 @@ +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<CustomExposureTriggerListener>() + private var isTriggering = false + private val pendingActions = LinkedList<PendingAction>() + + 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 + ) +} diff --git a/qmui/src/main/java/com/qmuiteam/qmui/exposure/ExposureContainer.kt b/qmui/src/main/java/com/qmuiteam/qmui/exposure/ExposureContainer.kt new file mode 100644 index 000000000..275fc11a0 --- /dev/null +++ b/qmui/src/main/java/com/qmuiteam/qmui/exposure/ExposureContainer.kt @@ -0,0 +1,14 @@ +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 + } +} \ No newline at end of file diff --git a/qmui/src/main/java/com/qmuiteam/qmui/exposure/ExposureEffect.kt b/qmui/src/main/java/com/qmuiteam/qmui/exposure/ExposureEffect.kt new file mode 100644 index 000000000..7df16e636 --- /dev/null +++ b/qmui/src/main/java/com/qmuiteam/qmui/exposure/ExposureEffect.kt @@ -0,0 +1,119 @@ +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<Pair<Exposure, Long>>() + private val zombieSet = mutableSetOf<Pair<Exposure, Long>>() + + 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<ExposureEffect> +) \ No newline at end of file diff --git a/qmui/src/main/java/com/qmuiteam/qmui/exposure/ExposureEx.kt b/qmui/src/main/java/com/qmuiteam/qmui/exposure/ExposureEx.kt new file mode 100644 index 000000000..d10acb41b --- /dev/null +++ b/qmui/src/main/java/com/qmuiteam/qmui/exposure/ExposureEx.kt @@ -0,0 +1,325 @@ +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<ExposureEffect>() + 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) + } + } + } +} + diff --git a/qmui/src/main/java/com/qmuiteam/qmui/kotlin/ViewKt.kt b/qmui/src/main/java/com/qmuiteam/qmui/kotlin/ViewKt.kt index a84992fd9..cb54cea52 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/kotlin/ViewKt.kt +++ b/qmui/src/main/java/com/qmuiteam/qmui/kotlin/ViewKt.kt @@ -1,14 +1,53 @@ 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 = System.currentTimeMillis() + 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) @@ -47,9 +86,19 @@ fun View.onDebounceClick(wait: Long = 200, block: ((View) -> Unit)) { setOnClickListener(debounceClick(wait, block)) } -fun View.skin(block:(QMUISkinValueBuilder.() -> Unit)){ - val builder = QMUISkinValueBuilder.acquire(); +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, "") +} diff --git a/qmui/src/main/java/com/qmuiteam/qmui/layout/QMUIConstraintLayout.java b/qmui/src/main/java/com/qmuiteam/qmui/layout/QMUIConstraintLayout.java index 3ffa12b8a..7ea40a134 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/layout/QMUIConstraintLayout.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/layout/QMUIConstraintLayout.java @@ -20,11 +20,10 @@ import android.graphics.Canvas; import android.util.AttributeSet; -import com.qmuiteam.qmui.alpha.QMUIAlphaConstraintLayout; -import com.qmuiteam.qmui.alpha.QMUIAlphaLinearLayout; - import androidx.annotation.ColorInt; +import com.qmuiteam.qmui.alpha.QMUIAlphaConstraintLayout; + /** * @author cginechen * @date 2017-03-10 @@ -288,9 +287,14 @@ public float getShadowAlpha() { @Override public void dispatchDraw(Canvas canvas) { - super.dispatchDraw(canvas); - mLayoutHelper.drawDividers(canvas, getWidth(), getHeight()); - mLayoutHelper.dispatchRoundBorderDraw(canvas); + try { + super.dispatchDraw(canvas); + mLayoutHelper.drawDividers(canvas, getWidth(), getHeight()); + mLayoutHelper.dispatchRoundBorderDraw(canvas); + }catch (Throwable ignore){ + // unreasonable crash + } + } @Override @@ -317,4 +321,5 @@ public boolean hasRightSeparator() { public boolean hasBottomSeparator() { return mLayoutHelper.hasBottomSeparator(); } + } \ No newline at end of file diff --git a/qmui/src/main/java/com/qmuiteam/qmui/layout/QMUILayoutHelper.java b/qmui/src/main/java/com/qmuiteam/qmui/layout/QMUILayoutHelper.java index daf96344b..b639daef3 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/layout/QMUILayoutHelper.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/layout/QMUILayoutHelper.java @@ -45,7 +45,7 @@ * @date 2017-03-10 */ -public class QMUILayoutHelper implements IQMUILayout { +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; @@ -451,7 +451,12 @@ public void getOutline(View view, Outline outline) { if (w == 0 || h == 0) { return; } - int radius = getRealRadius(); + 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) { @@ -776,7 +781,7 @@ public void dispatchRoundBorderDraw(Canvas canvas) { return; } - int width = canvas.getWidth(), height = canvas.getHeight(); + int width = owner.getWidth(), height = owner.getHeight(); canvas.save(); canvas.translate(owner.getScrollX(), owner.getScrollY()); diff --git a/qmui/src/main/java/com/qmuiteam/qmui/layout/QMUIPriorityLinearLayout.java b/qmui/src/main/java/com/qmuiteam/qmui/layout/QMUIPriorityLinearLayout.java index 0fa124168..09da41fd6 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/layout/QMUIPriorityLinearLayout.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/layout/QMUIPriorityLinearLayout.java @@ -164,7 +164,7 @@ private void handleVertical(int widthMeasureSpec, int heightMeasureSpec) { // no space for disposableChild for (View view : mTempDisposableChildList) { LayoutParams lp = (LayoutParams) view.getLayoutParams(); - lp.width = 0; + lp.height = 0; lp.topMargin = 0; lp.bottomMargin = 0; } @@ -314,11 +314,6 @@ public LinearLayout.LayoutParams generateLayoutParams(AttributeSet attrs) { return new LayoutParams(getContext(), attrs); } - @Override - protected LinearLayout.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); diff --git a/qmui/src/main/java/com/qmuiteam/qmui/link/QMUILinkTouchDecorHelper.java b/qmui/src/main/java/com/qmuiteam/qmui/link/QMUILinkTouchDecorHelper.java index 6231cb2ae..3d38c9e16 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/link/QMUILinkTouchDecorHelper.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/link/QMUILinkTouchDecorHelper.java @@ -23,7 +23,7 @@ import android.view.MotionEvent; import android.widget.TextView; -import com.qmuiteam.qmui.BuildConfig; +import com.qmuiteam.qmui.QMUIConfig; import com.qmuiteam.qmui.widget.textview.ISpanTouchFix; import java.lang.ref.WeakReference; @@ -137,7 +137,7 @@ public ITouchableSpan getPressedSpan(TextView textView, Spannable spannable, Mot } return touchedSpan; } catch (IndexOutOfBoundsException e) { - if (BuildConfig.DEBUG) { + if (QMUIConfig.DEBUG) { Log.d(this.toString(), "getPressedSpan", e); } } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedBottomAreaBehavior.java b/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedBottomAreaBehavior.java index a1f68536d..cfc06eb1f 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedBottomAreaBehavior.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedBottomAreaBehavior.java @@ -16,7 +16,9 @@ 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; @@ -38,6 +40,13 @@ 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; diff --git a/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedBottomRecyclerView.java b/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedBottomRecyclerView.java index d0e29f322..430566c19 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedBottomRecyclerView.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedBottomRecyclerView.java @@ -85,11 +85,15 @@ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { @Override public void consumeScroll(int yUnconsumed) { if (yUnconsumed == Integer.MIN_VALUE) { - scrollToPosition(0); + if(canScrollVertically(-1)){ + scrollToPosition(0); + } } else if (yUnconsumed == Integer.MAX_VALUE) { - Adapter adapter = getAdapter(); - if (adapter != null) { - scrollToPosition(adapter.getItemCount() - 1); + if(canScrollVertically(1)) { + Adapter adapter = getAdapter(); + if (adapter != null) { + scrollToPosition(adapter.getItemCount() - 1); + } } } else { boolean reStartNestedScroll = false; diff --git a/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedScrollLayout.java b/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedScrollLayout.java index 94c6524cd..cc8b237ab 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedScrollLayout.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedScrollLayout.java @@ -25,15 +25,15 @@ 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; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.coordinatorlayout.widget.CoordinatorLayout; - public class QMUIContinuousNestedScrollLayout extends CoordinatorLayout implements QMUIContinuousNestedTopAreaBehavior.Callback, QMUIDraggableScrollBar.Callback { public static final String KEY_SCROLL_INFO_OFFSET = "@qmui_nested_scroll_layout_offset"; @@ -331,8 +331,7 @@ public void scrollBottomViewToTop() { int contentHeight = mBottomView.getContentHeight(); if (contentHeight != IQMUIContinuousNestedBottomView.HEIGHT_IS_ENOUGH_TO_SCROLL) { - mTopAreaBehavior.setTopAndBottomOffset( - getHeight() - contentHeight - ((View) mTopView).getHeight()); + mTopAreaBehavior.setTopAndBottomOffset(Math.min(0, getHeight() - contentHeight - ((View) mTopView).getHeight())); } else { mTopAreaBehavior.setTopAndBottomOffset( getHeight() - ((View) mBottomView).getHeight() - ((View) mTopView).getHeight()); @@ -441,7 +440,13 @@ public int getOffsetCurrent() { } public int getOffsetRange() { - if (mTopView == null || mBottomView == null) { + 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(); diff --git a/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedTopAreaBehavior.java b/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedTopAreaBehavior.java index 7f4a51df4..a156ffa45 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedTopAreaBehavior.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedTopAreaBehavior.java @@ -24,6 +24,7 @@ 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; @@ -47,6 +48,7 @@ public class QMUIContinuousNestedTopAreaBehavior extends QMUIViewOffsetBehavior< private Callback mCallback; private boolean isInTouch = false; private boolean isInFlingOrScroll = false; + private boolean replaceCancelActionWithMoveActionForWebView = true; public QMUIContinuousNestedTopAreaBehavior(Context context) { this(context, null); @@ -58,6 +60,10 @@ public QMUIContinuousNestedTopAreaBehavior(Context context, AttributeSet attrs) mViewFlinger = new ViewFlinger(context); } + public void setReplaceCancelActionWithMoveActionForWebView(boolean replaceCancelActionWithMoveActionForWebView) { + this.replaceCancelActionWithMoveActionForWebView = replaceCancelActionWithMoveActionForWebView; + } + public void setCallback(Callback callback) { mCallback = callback; } @@ -112,6 +118,18 @@ public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, 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(); @@ -278,6 +296,19 @@ public boolean onMeasureChild(@NonNull CoordinatorLayout parent, @NonNull View c 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, diff --git a/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedTopRecyclerView.java b/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedTopRecyclerView.java index b77d13c04..b7655381f 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedTopRecyclerView.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedTopRecyclerView.java @@ -56,12 +56,16 @@ private void init(){ @Override public int consumeScroll(int dyUnconsumed) { if (dyUnconsumed == Integer.MIN_VALUE) { - scrollToPosition(0); + if(canScrollVertically(-1)){ + scrollToPosition(0); + } return Integer.MIN_VALUE; } else if (dyUnconsumed == Integer.MAX_VALUE) { - Adapter adapter = getAdapter(); - if (adapter != null) { - scrollToPosition(adapter.getItemCount() - 1); + if(canScrollVertically(1)){ + Adapter adapter = getAdapter(); + if (adapter != null) { + scrollToPosition(adapter.getItemCount() - 1); + } } return Integer.MAX_VALUE; } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedTopWebView.java b/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedTopWebView.java index 78c0fcdfe..2767a70f0 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedTopWebView.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedTopWebView.java @@ -17,15 +17,14 @@ package com.qmuiteam.qmui.nestedScroll; import android.content.Context; -import android.os.Build; 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; -import androidx.annotation.NonNull; - public class QMUIContinuousNestedTopWebView extends QMUIWebView implements IQMUIContinuousNestedTopView { public static final String KEY_SCROLL_INFO = "@qmui_scroll_info_top_webview"; @@ -106,10 +105,6 @@ public void restoreScrollInfo(@NonNull Bundle bundle) { } private void exec(final String jsCode) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - evaluateJavascript(jsCode, null); - } else { - loadUrl(jsCode); - } + evaluateJavascript(jsCode, null); } } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIDraggableScrollBar.java b/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIDraggableScrollBar.java index 117ee046a..8e47c718d 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIDraggableScrollBar.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIDraggableScrollBar.java @@ -39,15 +39,15 @@ import android.view.MotionEvent; import android.view.View; -import com.qmuiteam.qmui.R; -import com.qmuiteam.qmui.util.QMUIDisplayHelper; -import com.qmuiteam.qmui.util.QMUILangHelper; - 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}; @@ -71,6 +71,7 @@ public void run() { 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) { @@ -90,6 +91,10 @@ public void setCallback(Callback callback) { mCallback = callback; } + public void setAdjustDistanceWithAnimation(boolean adjustDistanceWithAnimation) { + mAdjustDistanceWithAnimation = adjustDistanceWithAnimation; + } + public void setKeepShownTime(int keepShownTime) { mKeepShownTime = keepShownTime; } @@ -237,7 +242,7 @@ protected void onDraw(Canvas canvas) { int totalWidth = getWidth(); int top = getScrollBarTopMargin() + (int) ((totalHeight - drawableHeight) * mPercent); int left = totalWidth - drawableWidth; - if (!mIsInDragging && mDrawableDrawTop > 0) { + if (!mIsInDragging && mDrawableDrawTop > 0 && mAdjustDistanceWithAnimation) { int moveDistance = top - mDrawableDrawTop; if (moveDistance > mAdjustMaxDistanceOnce && moveDistance < mAdjustDistanceProtection) { top = mDrawableDrawTop + mAdjustMaxDistanceOnce; diff --git a/qmui/src/main/java/com/qmuiteam/qmui/qqface/QMUIQQFaceCompiler.java b/qmui/src/main/java/com/qmuiteam/qmui/qqface/QMUIQQFaceCompiler.java index 9ae87b6ca..2b3f6bdca 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/qqface/QMUIQQFaceCompiler.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/qqface/QMUIQQFaceCompiler.java @@ -20,6 +20,9 @@ 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; @@ -30,9 +33,6 @@ import java.util.List; import java.util.Map; -import androidx.annotation.MainThread; -import androidx.annotation.NonNull; - /** * {@link QMUIQQFaceView} 的内容解析器,将文本内容解析成 {@link QMUIQQFaceView} 想要的数据格式。 * @@ -142,7 +142,9 @@ public int compare(QMUITouchableSpan o1, QMUITouchableSpan o2) { return elementList; } elementList = realCompile(text, start, end, spans, spanInfo); - mCache.put(text, elementList); + if(!hasClickableSpans && !inSpan){ + mCache.put(text, elementList); + } return elementList; } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/qqface/QMUIQQFaceView.java b/qmui/src/main/java/com/qmuiteam/qmui/qqface/QMUIQQFaceView.java index a888f9f7b..1fde7353d 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/qqface/QMUIQQFaceView.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/qqface/QMUIQQFaceView.java @@ -25,15 +25,18 @@ import android.graphics.Rect; import android.graphics.Typeface; import android.graphics.drawable.Drawable; -import android.os.Build; import android.text.TextPaint; import android.text.TextUtils; import android.util.AttributeSet; -import android.util.Log; 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; @@ -46,10 +49,6 @@ import java.util.HashMap; import java.util.List; -import androidx.annotation.ColorInt; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; - import static android.view.View.MeasureSpec.AT_MOST; /** @@ -702,21 +701,15 @@ private void calculateLinesInner(List<QMUIQQFaceCompiler.Element> elements, int if (mJumpHandleMeasureAndDraw) { break; } - if (mCurrentCalLine > mMaxLine && mEllipsize == TextUtils.TruncateAt.END - && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - // 针对4.x的手机,如果超过最大行数,就打断测量,但这样存在的问题是getLines获取不到真实的行数 + 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; - } else if (mCurrentCalWidth + mQQFaceSize == widthEnd) { - gotoCalNextLine(widthStart); - } else { - mCurrentCalWidth += mQQFaceSize; } + mCurrentCalWidth += mQQFaceSize; if (widthEnd - widthStart < mQQFaceSize) { // 一个表情的宽度都容不下 mJumpHandleMeasureAndDraw = true; @@ -820,6 +813,13 @@ 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(); @@ -887,9 +887,6 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { break; } setMeasuredDimension(width, height); - Log.v(TAG, "mLines = " + mLines + " ; width = " + width + " ; height = " - + height + " ; maxLine = " + maxLine + "; measure time = " - + (System.currentTimeMillis() - start)); } @Override @@ -898,15 +895,12 @@ protected void onDraw(Canvas canvas) { return; } pickTextPaintColor(); - - long start = System.currentTimeMillis(); List<QMUIQQFaceCompiler.Element> elements = mElementList.getElements(); mCurrentDrawBaseLine = getPaddingTop() + mFirstBaseLine; mCurrentDrawLine = 1; setStartDrawUsedWidth(getPaddingLeft(), getWidth() - getPaddingLeft() - getPaddingRight()); mIsExecutedMiddleEllipsize = false; drawElements(canvas, elements, getWidth() - getPaddingLeft() - getPaddingRight()); - Log.v(TAG, "onDraw spend time = " + (System.currentTimeMillis() - start)); } private void pickTextPaintColor() { @@ -1335,7 +1329,7 @@ private void drawText(Canvas canvas, CharSequence text, int start, int end, int } private void onDrawQQFace(Canvas canvas, int res, @Nullable Drawable specialDrawable, int widthStart, int widthEnd, boolean isFirst, boolean isLast) { - int size = res != -1 || specialDrawable == null ? mQQFaceSize : specialDrawable.getIntrinsicWidth() + (isFirst || isLast ? mSpecialDrawablePadding : mSpecialDrawablePadding * 2); + 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) { diff --git a/qmui/src/main/java/com/qmuiteam/qmui/recyclerView/QMUIRVDraggableScrollBar.java b/qmui/src/main/java/com/qmuiteam/qmui/recyclerView/QMUIRVDraggableScrollBar.java index b94bb4357..cf61809b2 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/recyclerView/QMUIRVDraggableScrollBar.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/recyclerView/QMUIRVDraggableScrollBar.java @@ -27,6 +27,7 @@ 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; @@ -44,6 +45,7 @@ public class QMUIRVDraggableScrollBar extends RecyclerView.ItemDecoration implem 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; @@ -119,7 +121,9 @@ public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEv } } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { if (mIsInDragging) { - onDragging(rv, mScrollBarDrawable, x, y); + if(action == MotionEvent.ACTION_UP){ + onDragging(rv, mScrollBarDrawable, x, y); + } endDrag(); } } @@ -146,7 +150,9 @@ public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { } } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { if (mIsInDragging) { - onDragging(rv, mScrollBarDrawable, x, y); + if(action == MotionEvent.ACTION_UP) { + onDragging(rv, mScrollBarDrawable, x, y); + } endDrag(); } } @@ -345,13 +351,19 @@ private void onDragging(RecyclerView recyclerView, Drawable drawable, int x, int recyclerView.scrollToPosition(adapter.getItemCount() - 1); } } 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); + 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(); @@ -451,6 +463,12 @@ private int getCurrentOffset(@NonNull RecyclerView recyclerView) { } 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); } @@ -478,7 +496,7 @@ public void handle(@NotNull @NonNull RecyclerView recyclerView, invalidate(); } - interface Callback { + public interface Callback { void onDragStarted(); void onDragToPercent(float percent); diff --git a/qmui/src/main/java/com/qmuiteam/qmui/recyclerView/QMUIRVItemSwipeAction.java b/qmui/src/main/java/com/qmuiteam/qmui/recyclerView/QMUIRVItemSwipeAction.java index c331bef8c..d98446dda 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/recyclerView/QMUIRVItemSwipeAction.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/recyclerView/QMUIRVItemSwipeAction.java @@ -838,7 +838,7 @@ View findChildView(MotionEvent event) { 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, anim.mX, anim.mY)) { + if (hitTest(view, x, y, view.getX(), view.getY())) { return view; } } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/skin/IQMUISkinApplyListener.java b/qmui/src/main/java/com/qmuiteam/qmui/skin/IQMUISkinApplyListener.java new file mode 100644 index 000000000..90a459127 --- /dev/null +++ b/qmui/src/main/java/com/qmuiteam/qmui/skin/IQMUISkinApplyListener.java @@ -0,0 +1,25 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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); +} diff --git a/qmui/src/main/java/com/qmuiteam/qmui/skin/QMUISkinHelper.java b/qmui/src/main/java/com/qmuiteam/qmui/skin/QMUISkinHelper.java index 82cacb122..61ae8a8c6 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/skin/QMUISkinHelper.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/skin/QMUISkinHelper.java @@ -77,9 +77,9 @@ public static void setSkinValue(@NonNull View view, SkinWriter writer) { sSkinValueBuilder.clear(); } - public static void refreshRVItemDecoration(@NonNull RecyclerView view, IQMUISkinHandlerDecoration itemDecoration){ + public static void refreshRVItemDecoration(@NonNull RecyclerView view, IQMUISkinHandlerDecoration itemDecoration) { QMUISkinManager.ViewSkinCurrent skinCurrent = QMUISkinManager.getViewSkinCurrent(view); - if(skinCurrent != null){ + if (skinCurrent != null) { QMUISkinManager.of(skinCurrent.managerName, view.getContext()).refreshRecyclerDecoration(view, itemDecoration, skinCurrent.index); } } @@ -92,18 +92,18 @@ public static int getCurrentSkinIndex(@NonNull View view) { return QMUISkinManager.DEFAULT_SKIN; } - public static void refreshViewSkin(@NonNull View view){ + 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){ + 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)) { + if (!source.equals(skin)) { QMUISkinManager.of(source.managerName, view.getContext()).dispatch(view, source.index); } } @@ -114,7 +114,28 @@ public static void setSkinDefaultProvider(@NonNull View view, view.setTag(R.id.qmui_skin_default_attr_provider, provider); } - public static void warnRuleNotSupport(View view, String rule){ + 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); } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/skin/QMUISkinLayoutInflaterFactory.java b/qmui/src/main/java/com/qmuiteam/qmui/skin/QMUISkinLayoutInflaterFactory.java index 4a744a9ce..0e86b9e0d 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/skin/QMUISkinLayoutInflaterFactory.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/skin/QMUISkinLayoutInflaterFactory.java @@ -15,24 +15,28 @@ */ 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; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; - public class QMUISkinLayoutInflaterFactory implements LayoutInflater.Factory2 { private static final String TAG = "QMUISkin"; private static final String[] sClassPrefixList = { @@ -43,6 +47,12 @@ public class QMUISkinLayoutInflaterFactory implements LayoutInflater.Factory2 { }; private static final HashMap<String, String> 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<Activity> mActivityWeakReference; private LayoutInflater mOriginLayoutInflater; @@ -75,15 +85,35 @@ public View onCreateView(View parent, String name, Context context, AttributeSet .createView(name, sSuccessClassNamePrefixMap.get(name), attrs); }else{ for (String prefix : sClassPrefixList) { - view = mOriginLayoutInflater.createView(name, prefix, attrs); - if (view != null) { - sSuccessClassNamePrefixMap.put(name, prefix); - break; + try { + view = mOriginLayoutInflater.createView(name, prefix, attrs); + if (view != null) { + sSuccessClassNamePrefixMap.put(name, prefix); + break; + } + } catch (Exception ignored) { } } } }else{ - view = mOriginLayoutInflater.cloneInContext(context).createView(name, null, attrs); + 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){ @@ -104,6 +134,19 @@ public View onCreateView(View parent, String name, Context context, AttributeSet 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); diff --git a/qmui/src/main/java/com/qmuiteam/qmui/skin/QMUISkinManager.java b/qmui/src/main/java/com/qmuiteam/qmui/skin/QMUISkinManager.java index 290f0b964..d86566d96 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/skin/QMUISkinManager.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/skin/QMUISkinManager.java @@ -19,7 +19,6 @@ import android.app.Dialog; import android.content.Context; import android.content.res.Resources; -import android.os.Build; import android.os.Trace; import android.text.Spanned; import android.util.ArrayMap; @@ -31,7 +30,15 @@ import android.widget.PopupWindow; import android.widget.TextView; -import com.qmuiteam.qmui.BuildConfig; +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; @@ -59,24 +66,37 @@ import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Objects; -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; - 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<String, QMUISkinManager> 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) { @@ -87,15 +107,15 @@ public static QMUISkinManager defaultInstance(Context context) { @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); + if (instance == null) { + instance = new QMUISkinManager(name, resources, packageName); sInstances.put(name, instance); } return instance; } @MainThread - public static QMUISkinManager of(String name, Context context){ + public static QMUISkinManager of(String name, Context context) { context = context.getApplicationContext(); return of(name, context.getResources(), context.getPackageName()); } @@ -109,6 +129,7 @@ public static QMUISkinManager of(String name, Context context){ private SparseArray<SkinItem> mSkins = new SparseArray<>(); private static HashMap<String, IQMUISkinRuleHandler> sRuleHandlers = new HashMap<>(); private static HashMap<Integer, Resources.Theme> sStyleIdThemeMap = new HashMap<>(); + private boolean mIsInSkinChangeDispatch = false; static { sRuleHandlers.put(QMUISkinValueBuilder.BACKGROUND, new QMUISkinRuleBackgroundHandler()); @@ -138,7 +159,7 @@ public static QMUISkinManager of(String name, Context context){ sRuleHandlers.put(QMUISkinValueBuilder.MORE_BG_COLOR, new QMUISkinRuleMoreBgColorHandler()); } - public static void setRuleHandler(String name, IQMUISkinRuleHandler handler){ + public static void setRuleHandler(String name, IQMUISkinRuleHandler handler) { sRuleHandlers.put(name, handler); } @@ -189,7 +210,6 @@ public void onChildViewRemoved(View parent, View child) { }; - public QMUISkinManager(String name, Resources resources, String packageName) { mName = name; mResources = resources; @@ -234,9 +254,9 @@ public void addSkin(int index, int styleRes) { mSkins.append(index, skinItem); } - static ViewSkinCurrent getViewSkinCurrent(View view){ + static ViewSkinCurrent getViewSkinCurrent(View view) { Object current = view.getTag(R.id.qmui_skin_current); - if(current instanceof ViewSkinCurrent){ + if (current instanceof ViewSkinCurrent) { return (ViewSkinCurrent) current; } return null; @@ -246,7 +266,7 @@ public void dispatch(View view, int skinIndex) { if (view == null) { return; } - if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (QMUIConfig.DEBUG) { Trace.beginSection("QMUISkin::dispatch"); } SkinItem skinItem = mSkins.get(skinIndex); @@ -260,7 +280,7 @@ public void dispatch(View view, int skinIndex) { theme = skinItem.getTheme(); } runDispatch(view, skinIndex, theme); - if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (QMUIConfig.DEBUG) { Trace.endSection(); } } @@ -268,7 +288,7 @@ public void dispatch(View view, int skinIndex) { 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)){ + if (currentTheme != null && currentTheme.index == skinIndex && Objects.equals(currentTheme.managerName, mName)) { return; } view.setTag(R.id.qmui_skin_current, new ViewSkinCurrent(mName, skinIndex)); @@ -278,10 +298,20 @@ private void runDispatch(@NonNull View view, int skinIndex, Resources.Theme them return; } } - applyTheme(view, skinIndex, theme); + + 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 (useHierarchyChangeListener(viewGroup)) { + if (sDispatchListenStrategySelector.select(viewGroup) == DispatchListenStrategy.LISTEN_ON_HIERARCHY_CHANGE) { viewGroup.setOnHierarchyChangeListener(mOnHierarchyChangeListener); } else { viewGroup.addOnLayoutChangeListener(mOnLayoutChangeListener); @@ -289,7 +319,7 @@ private void runDispatch(@NonNull View view, int skinIndex, Resources.Theme them for (int i = 0; i < viewGroup.getChildCount(); i++) { runDispatch(viewGroup.getChildAt(i), skinIndex, theme); } - } else if ((view instanceof TextView) || (view instanceof QMUIQQFaceView)) { + } else if (!ignoreApply && ((view instanceof TextView) || (view instanceof QMUIQQFaceView))) { CharSequence text; if (view instanceof TextView) { text = ((TextView) view).getText(); @@ -308,21 +338,20 @@ private void runDispatch(@NonNull View view, int skinIndex, Resources.Theme them } } - private boolean useHierarchyChangeListener(ViewGroup viewGroup) { - return viewGroup instanceof RecyclerView || - viewGroup instanceof ViewPager || - viewGroup instanceof AdapterView || - viewGroup.getClass().isAnnotationPresent(QMUISkinListenWithHierarchyChange.class); - } - private void applyTheme(@NonNull View view, int skinIndex, Resources.Theme theme) { SimpleArrayMap<String, Integer> attrs = getSkinAttrs(view); - try{ + 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(); @@ -333,7 +362,7 @@ private void applyTheme(@NonNull View view, int skinIndex, Resources.Theme theme } } } - }catch (Throwable throwable){ + } catch (Throwable throwable) { QMUILog.printErrStackTrace(TAG, throwable, "catch error when apply theme: " + view.getClass().getSimpleName() + "; " + skinIndex + "; attrs = " + (attrs == null ? "null" : attrs.toString())); @@ -341,8 +370,8 @@ private void applyTheme(@NonNull View view, int skinIndex, Resources.Theme theme } void refreshRecyclerDecoration(@NonNull RecyclerView recyclerView, - @NonNull IQMUISkinHandlerDecoration decoration, - int skinIndex){ + @NonNull IQMUISkinHandlerDecoration decoration, + int skinIndex) { SkinItem skinItem = mSkins.get(skinIndex); if (skinItem != null) { decoration.handle(recyclerView, this, skinIndex, skinItem.getTheme()); @@ -394,7 +423,7 @@ private SimpleArrayMap<String, Integer> getSkinAttrs(View view) { SimpleArrayMap<String, Integer> attrs = null; if (view instanceof IQMUISkinDefaultAttrProvider) { SimpleArrayMap<String, Integer> defaultAttrs = ((IQMUISkinDefaultAttrProvider) view).getDefaultSkinAttrs(); - if(defaultAttrs != null && !defaultAttrs.isEmpty()){ + if (defaultAttrs != null && !defaultAttrs.isEmpty()) { attrs = new SimpleArrayMap<>(defaultAttrs); } } @@ -402,7 +431,7 @@ private SimpleArrayMap<String, Integer> getSkinAttrs(View view) { R.id.qmui_skin_default_attr_provider); if (provider != null) { SimpleArrayMap<String, Integer> providedAttrs = provider.getDefaultSkinAttrs(); - if(providedAttrs != null && !providedAttrs.isEmpty()){ + if (providedAttrs != null && !providedAttrs.isEmpty()) { if (attrs != null) { attrs.putAll(providedAttrs); } else { @@ -412,7 +441,7 @@ private SimpleArrayMap<String, Integer> getSkinAttrs(View view) { } if (attrs == null) { - if(items.length <= 0){ + if (items.length <= 0) { return null; } attrs = new SimpleArrayMap<>(items.length); @@ -468,7 +497,7 @@ Resources.Theme getTheme() { private int mCurrentSkin = DEFAULT_SKIN; private final List<WeakReference<?>> mSkinObserverList = new ArrayList<>(); - private final List<WeakReference<OnSkinChangeListener>> mSkinChangeListeners = new ArrayList<>(); + private final List<OnSkinChangeListener> mSkinChangeListeners = new ArrayList<>(); public void register(@NonNull Activity activity) { if (!containSkinObserver(activity)) { @@ -564,12 +593,14 @@ private boolean containSkinObserver(Object object) { 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) { @@ -598,40 +629,25 @@ public void changeSkin(int index) { } for (int i = mSkinChangeListeners.size() - 1; i >= 0; i--) { - OnSkinChangeListener item = mSkinChangeListeners.get(i).get(); - if (item == null) { - mSkinChangeListeners.remove(i); - } else { - item.onSkinChange(this, oldIndex, mCurrentSkin); - } + OnSkinChangeListener item = mSkinChangeListeners.get(i); + item.onSkinChange(this, oldIndex, mCurrentSkin); } + mIsInSkinChangeDispatch = false; } + @MainThread public void addSkinChangeListener(@NonNull OnSkinChangeListener listener) { - Iterator<WeakReference<OnSkinChangeListener>> iterator = mSkinChangeListeners.iterator(); - while (iterator.hasNext()) { - Object item = iterator.next().get(); - if (item != null) { - return; - } else { - iterator.remove(); - } + if (mIsInSkinChangeDispatch) { + throw new RuntimeException("Can not add skinChangeListener while dispatching"); } - mSkinChangeListeners.add(new WeakReference<>(listener)); + mSkinChangeListeners.add(listener); } public void removeSkinChangeListener(@NonNull OnSkinChangeListener listener) { - Iterator<WeakReference<OnSkinChangeListener>> iterator = mSkinChangeListeners.iterator(); - while (iterator.hasNext()) { - Object item = iterator.next().get(); - if (item != null) { - if (item == listener) { - iterator.remove(); - } - } else { - iterator.remove(); - } + if (mIsInSkinChangeDispatch) { + throw new RuntimeException("Can not add skinChangeListener while dispatching"); } + mSkinChangeListeners.remove(listener); } public int getCurrentSkin() { @@ -642,10 +658,11 @@ public interface OnSkinChangeListener { void onSkinChange(QMUISkinManager skinManager, int oldSkin, int newSkin); } - class ViewSkinCurrent{ + class ViewSkinCurrent { String managerName; int index; - ViewSkinCurrent(String managerName, int index){ + + ViewSkinCurrent(String managerName, int index) { this.managerName = managerName; this.index = index; } @@ -664,4 +681,14 @@ 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 + } } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/skin/handler/QMUISkinRuleHintColorHandler.java b/qmui/src/main/java/com/qmuiteam/qmui/skin/handler/QMUISkinRuleHintColorHandler.java index 442c93a50..47c401b5f 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/skin/handler/QMUISkinRuleHintColorHandler.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/skin/handler/QMUISkinRuleHintColorHandler.java @@ -18,7 +18,7 @@ protected void handle(@NotNull View view, @NotNull String name, ColorStateList c } else if (view instanceof TextInputLayout) { ((TextInputLayout) view).setHintTextColor(colorStateList); }else if(view instanceof QMUISlider){ - ((QMUISlider)view).setBarProgressColor(colorStateList.getDefaultColor()); + ((QMUISlider)view).setRecordProgressColor(colorStateList.getDefaultColor()); }else{ QMUISkinHelper.warnRuleNotSupport(view, name); } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/span/QMUICustomTypefaceSpan.java b/qmui/src/main/java/com/qmuiteam/qmui/span/QMUICustomTypefaceSpan.java index 8e0118952..b46d4f5be 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/span/QMUICustomTypefaceSpan.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/span/QMUICustomTypefaceSpan.java @@ -61,7 +61,7 @@ private static void applyCustomTypeFace(Paint paint, @Nullable Typeface tf) { int oldStyle; Typeface old = paint.getTypeface(); if (old == null) { - oldStyle = 0; + oldStyle = Typeface.NORMAL; } else { oldStyle = old.getStyle(); } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/span/QMUITextSizeSpan.java b/qmui/src/main/java/com/qmuiteam/qmui/span/QMUITextSizeSpan.java index 47657ad0a..ac8c380da 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/span/QMUITextSizeSpan.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/span/QMUITextSizeSpan.java @@ -19,9 +19,10 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Typeface; -import androidx.annotation.NonNull; import android.text.style.ReplacementSpan; +import androidx.annotation.NonNull; + /** * 支持调整字体大小的 span。{@link android.text.style.AbsoluteSizeSpan} 可以调整字体大小,但在中英文混排下由于 decent 的不同, * 无法根据具体需求进行底部对齐或者顶部对齐。而 QMUITextSizeSpan 则可以多传一个参数,让你可以根据具体情况来决定偏移值。 @@ -44,13 +45,13 @@ 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) { - mPaint = new Paint(paint); - mPaint.setTextSize(mTextSize); - mPaint.setTypeface(mTypeface); if(mTextSize > paint.getTextSize() && fm != null){ Paint.FontMetricsInt newFm = mPaint.getFontMetricsInt(); fm.descent = newFm.descent; @@ -64,6 +65,9 @@ public int getSize(@NonNull Paint paint, CharSequence text, int start, int 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); } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/util/OnceReadValue.java b/qmui/src/main/java/com/qmuiteam/qmui/util/OnceReadValue.java new file mode 100644 index 000000000..a52cf6b98 --- /dev/null +++ b/qmui/src/main/java/com/qmuiteam/qmui/util/OnceReadValue.java @@ -0,0 +1,22 @@ +package com.qmuiteam.qmui.util; + +public abstract class OnceReadValue<P, T> { + + 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); +} diff --git a/qmui/src/main/java/com/qmuiteam/qmui/util/QMUICollapsingTextHelper.java b/qmui/src/main/java/com/qmuiteam/qmui/util/QMUICollapsingTextHelper.java index 5f632b5a0..2e2289ab8 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/util/QMUICollapsingTextHelper.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/util/QMUICollapsingTextHelper.java @@ -48,14 +48,14 @@ import android.view.View; import android.view.animation.Interpolator; -import com.qmuiteam.qmui.R; - 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 @@ -106,6 +106,7 @@ public final class QMUICollapsingTextHelper { private Typeface mCollapsedTypeface; private Typeface mExpandedTypeface; private Typeface mCurrentTypeface; + private float mTypefaceUpdateAreaPercent; private CharSequence mText; private CharSequence mTextToDraw; @@ -225,9 +226,7 @@ public void setCollapsedTextAppearance(int resId) { mCollapsedShadowRadius = a.getFloat(R.styleable.QMUITextAppearance_android_shadowRadius, 0); a.recycle(); - if (Build.VERSION.SDK_INT >= 16) { - mCollapsedTypeface = readFontFamilyTypeface(resId); - } + mCollapsedTypeface = readFontFamilyTypeface(resId); recalculate(); } @@ -251,9 +250,7 @@ public void setExpandedTextAppearance(int resId) { R.styleable.QMUITextAppearance_android_shadowRadius, 0); a.recycle(); - if (Build.VERSION.SDK_INT >= 16) { - mExpandedTypeface = readFontFamilyTypeface(resId); - } + mExpandedTypeface = readFontFamilyTypeface(resId); recalculate(); } @@ -391,6 +388,10 @@ public final boolean setState(final int[] state) { return false; } + public void setTypefaceUpdateAreaPercent(float typefaceUpdateAreaPercent) { + mTypefaceUpdateAreaPercent = typefaceUpdateAreaPercent; + } + public final boolean isStateful() { return (mCollapsedTextColor != null && mCollapsedTextColor.isStateful()) || (mExpandedTextColor != null && mExpandedTextColor.isStateful()); @@ -631,12 +632,12 @@ private void calculateUsingTextSize(final float textSize) { final float newTextSize; boolean updateDrawText = false; - if(mExpandedFraction == 1f){ + if(mExpandedFraction >= 1f - mTypefaceUpdateAreaPercent){ if (mCurrentTypeface != mCollapsedTypeface) { mCurrentTypeface = mCollapsedTypeface; updateDrawText = true; } - }else if(mExpandedFraction == 0f){ + }else if(mExpandedFraction <= mTypefaceUpdateAreaPercent){ if (mCurrentTypeface != mExpandedTypeface) { mCurrentTypeface = mExpandedTypeface; updateDrawText = true; diff --git a/qmui/src/main/java/com/qmuiteam/qmui/util/QMUIDeviceHelper.java b/qmui/src/main/java/com/qmuiteam/qmui/util/QMUIDeviceHelper.java index 5acdcf48b..f379ca982 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/util/QMUIDeviceHelper.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/util/QMUIDeviceHelper.java @@ -17,21 +17,30 @@ package com.qmuiteam.qmui.util; import android.annotation.SuppressLint; -import android.annotation.TargetApi; +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 androidx.annotation.Nullable; +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; @@ -48,15 +57,36 @@ public class QMUIDeviceHelper { private final static String FLYME = "flyme"; private final static String ZTEC2016 = "zte c2016"; private final static String ZUKZ1 = "zuk z1"; - private final static String ESSENTIAL = "essential"; 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(); - - static { + 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) { @@ -105,45 +135,59 @@ public static boolean isTablet(Context context) { /** * 判断是否是flyme系统 */ + private static OnceReadValue<Void, Boolean> isFlymeValue = new OnceReadValue<Void, Boolean>() { + @Override + protected Boolean read(Void param) { + checkReadInfo(); + return !TextUtils.isEmpty(sFlymeVersionName) && sFlymeVersionName.contains(FLYME); + } + }; public static boolean isFlyme() { - return !TextUtils.isEmpty(sFlymeVersionName) && sFlymeVersionName.contains(FLYME); + 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){ + 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{ + try { Pattern pattern = Pattern.compile("(\\d+\\.){2}\\d"); Matcher matcher = pattern.matcher(sFlymeVersionName); if (matcher.find()) { @@ -156,20 +200,20 @@ public static boolean isFlymeLowerThan(int majorVersion, int minorVersion, int p } } - if(version.length >= 2 && minorVersion > 0){ + if (version.length >= 2 && minorVersion > 0) { if (Integer.parseInt(version[1]) < majorVersion) { isLower = true; } } - if(version.length >= 3 && patchVersion > 0){ + if (version.length >= 3 && patchVersion > 0) { if (Integer.parseInt(version[2]) < majorVersion) { isLower = true; } } } } - }catch (Throwable ignore){ + } catch (Throwable ignore) { } } @@ -177,50 +221,83 @@ public static boolean isFlymeLowerThan(int majorVersion, int minorVersion, int p } + private static OnceReadValue<Void, Boolean> isMeizuValue = new OnceReadValue<Void, Boolean>() { + @Override + protected Boolean read(Void param) { + checkReadInfo(); + return isPhone(MEIZUBOARD) || isFlyme(); + } + }; public static boolean isMeizu() { - return isPhone(MEIZUBOARD) || isFlyme(); + return isMeizuValue.get(null); } /** * 判断是否为小米 * https://dev.mi.com/doc/?p=254 */ + private static OnceReadValue<Void, Boolean> isXiaomiValue = new OnceReadValue<Void, Boolean>() { + @Override + protected Boolean read(Void param) { + return Build.MANUFACTURER.toLowerCase().equals("xiaomi"); + } + }; public static boolean isXiaomi() { - return Build.MANUFACTURER.toLowerCase().equals("xiaomi"); + return isXiaomiValue.get(null); } + private static OnceReadValue<Void, Boolean> isVivoValue = new OnceReadValue<Void, Boolean>() { + @Override + protected Boolean read(Void param) { + return BRAND.contains("vivo") || BRAND.contains("bbk"); + } + }; public static boolean isVivo() { - return BRAND.contains("vivo") || BRAND.contains("bbk"); + return isVivoValue.get(null); } + private static OnceReadValue<Void, Boolean> isOppoValue = new OnceReadValue<Void, Boolean>() { + @Override + protected Boolean read(Void param) { + return BRAND.contains("oppo"); + } + }; public static boolean isOppo() { - return BRAND.contains("oppo"); + return isOppoValue.get(null); } + private static OnceReadValue<Void, Boolean> isHuaweiValue = new OnceReadValue<Void, Boolean>() { + @Override + protected Boolean read(Void param) { + return BRAND.contains("huawei") || BRAND.contains("honor"); + } + }; public static boolean isHuawei() { - return BRAND.contains("huawei") || BRAND.contains("honor"); + return isHuaweiValue.get(null); } - public static boolean isEssentialPhone(){ - return BRAND.contains("essential"); - } - - - /** - * 判断是否为 ZUK Z1 和 ZTK C2016。 - * 两台设备的系统虽然为 android 6.0,但不支持状态栏icon颜色改变,因此经常需要对它们进行额外判断。 - */ - public static boolean isZUKZ1() { - final String board = android.os.Build.MODEL; - return board != null && board.toLowerCase().contains(ZUKZ1); + private static OnceReadValue<Void, Boolean> isEssentialPhoneValue = new OnceReadValue<Void, Boolean>() { + @Override + protected Boolean read(Void param) { + return BRAND.contains("essential"); + } + }; + public static boolean isEssentialPhone() { + return isEssentialPhoneValue.get(null); } - public static boolean isZTKC2016() { - final String board = android.os.Build.MODEL; - return board != null && board.toLowerCase().contains(ZTEC2016); + private static OnceReadValue<Context, Boolean> isMiuiFullDisplayValue = new OnceReadValue<Context, Boolean>() { + @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; @@ -233,36 +310,138 @@ private static boolean isPhone(String[] boards) { 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; - if (version >= 19) { - return checkOp(context, 24); // 24 是AppOpsManager.OP_SYSTEM_ALERT_WINDOW 的值,该值无法直接访问 - } else { - try { - return (context.getApplicationInfo().flags & 1 << 27) == 1 << 27; - } catch (Exception e) { - e.printStackTrace(); - return false; - } + 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; } - @TargetApi(19) + private static boolean checkOp(Context context, int op) { - final int version = Build.VERSION.SDK_INT; - if (version >= Build.VERSION_CODES.KITKAT) { - 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(); - } + 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; } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/util/QMUIKeyboardHelper.java b/qmui/src/main/java/com/qmuiteam/qmui/util/QMUIKeyboardHelper.java index 5a670c9de..4fb9f9f88 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/util/QMUIKeyboardHelper.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/util/QMUIKeyboardHelper.java @@ -26,6 +26,14 @@ 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 @@ -92,6 +100,43 @@ public static boolean hideKeyboard(final View view) { 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<WindowInsetsAnimationCompat> 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<WindowInsetsAnimationCompat> 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. * diff --git a/qmui/src/main/java/com/qmuiteam/qmui/util/QMUILangHelper.java b/qmui/src/main/java/com/qmuiteam/qmui/util/QMUILangHelper.java index 81c9fb953..4d5d568d0 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/util/QMUILangHelper.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/util/QMUILangHelper.java @@ -21,6 +21,7 @@ import java.io.Closeable; import java.io.IOException; import java.util.Locale; +import java.util.Objects; /** * @author cginechen @@ -94,8 +95,9 @@ public static void close(Closeable c) { } } + @Deprecated public static boolean objectEquals(Object a, Object b) { - return (a == b) || (a != null && a.equals(b)); + return Objects.equals(a, b); } public static int constrain(int amount, int low, int high) { diff --git a/qmui/src/main/java/com/qmuiteam/qmui/util/QMUINotchHelper.java b/qmui/src/main/java/com/qmuiteam/qmui/util/QMUINotchHelper.java index 7b7efeb19..e25791416 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/util/QMUINotchHelper.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/util/QMUINotchHelper.java @@ -31,9 +31,11 @@ import android.view.WindowInsets; import android.view.WindowManager; -import java.lang.reflect.Method; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; -import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import java.lang.reflect.Method; public class QMUINotchHelper { @@ -274,15 +276,12 @@ private static void getOfficialSafeInsetRect(View view, Rect out) { if(view == null){ return; } - WindowInsets rootWindowInsets = view.getRootWindowInsets(); + WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(view); if(rootWindowInsets == null){ return; } - DisplayCutout displayCutout = rootWindowInsets.getDisplayCutout(); - if(displayCutout != null){ - out.set(displayCutout.getSafeInsetLeft(), displayCutout.getSafeInsetTop(), - displayCutout.getSafeInsetRight(), displayCutout.getSafeInsetBottom()); - } + 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){ diff --git a/qmui/src/main/java/com/qmuiteam/qmui/util/QMUIReflectHelper.java b/qmui/src/main/java/com/qmuiteam/qmui/util/QMUIReflectHelper.java new file mode 100644 index 000000000..4a41e7749 --- /dev/null +++ b/qmui/src/main/java/com/qmuiteam/qmui/util/QMUIReflectHelper.java @@ -0,0 +1,240 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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> 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> 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> 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> 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> 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> T invokeStaticMethod(final Class<?> klass, final String name) { + return invokeStaticMethod(klass, name, new Class[0], new Object[0]); + } + + @SuppressWarnings("unchecked") + public static <T> 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> T invokeMethod(final Object obj, final String name) { + return invokeMethod(obj, name, new Class[0], new Object[0]); + } + + @SuppressWarnings("unchecked") + public static <T> 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); + } + } +} diff --git a/qmui/src/main/java/com/qmuiteam/qmui/util/QMUIStatusBarHelper.java b/qmui/src/main/java/com/qmuiteam/qmui/util/QMUIStatusBarHelper.java index 75807d8b2..a3d20cfea 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/util/QMUIStatusBarHelper.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/util/QMUIStatusBarHelper.java @@ -21,34 +21,37 @@ import android.content.Context; import android.graphics.Color; import android.os.Build; -import androidx.annotation.ColorInt; -import androidx.annotation.IntDef; -import androidx.core.view.ViewCompat; import android.view.View; import android.view.Window; import android.view.WindowManager; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; +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 final static int STATUSBAR_TYPE_DEFAULT = 0; - private final static int STATUSBAR_TYPE_MIUI = 1; - private final static int STATUSBAR_TYPE_FLYME = 2; - private final static int STATUSBAR_TYPE_ANDROID6 = 3; // Android 6.0 + 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 int mStatusBarType = STATUSBAR_TYPE_DEFAULT; + private static StatusBarType mStatusBarType = StatusBarType.Default; private static Integer sTransparentValue; public static void translucent(Activity activity) { @@ -60,9 +63,8 @@ public static void translucent(Window window) { } private static boolean supportTranslucent() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT - // Essential Phone 在 Android 8 之前沉浸式做得不全,系统不从状态栏顶部开始布局却会下发 WindowInsets - && !(QMUIDeviceHelper.isEssentialPhone() && Build.VERSION.SDK_INT < 26); + // Essential Phone 在 Android 8 之前沉浸式做得不全,系统不从状态栏顶部开始布局却会下发 WindowInsets + return !(QMUIDeviceHelper.isEssentialPhone() && Build.VERSION.SDK_INT < 26); } /** @@ -87,46 +89,51 @@ public static void translucent(Window window, @ColorInt int colorOn5x) { handleDisplayCutoutMode(window); } - // 小米和魅族4.4 以上版本支持沉浸式 - // 小米 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; + 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; + } } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && supportTransclentStatusBar6()) { - // android 6以后可以改状态栏字体颜色,因此可以自行设置为透明 - // ZUK Z1是个另类,自家应用可以实现字体颜色变色,但没开放接口 - 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时背景不是半透明而是没有背景了。。。。。 + 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); - } -// } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { -// // android4.4的默认是从上到下黑到透明,我们的背景是白色,很难看,因此只做魅族和小米的 -// } else if(Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR1){ -// // 如果app 为白色,需要更改状态栏颜色,因此不能让19一下支持透明状态栏 -// Window window = activity.getWindow(); -// Integer transparentValue = getStatusBarAPITransparentValue(activity); -// if(transparentValue != null) { -// window.getDecorView().setSystemUiVisibility(transparentValue); -// } + // 采取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(); @@ -169,26 +176,20 @@ private static void realHandleDisplayCutoutMode(Window window, View decorView) { */ public static boolean setStatusBarLightMode(Activity activity) { if (activity == null) return false; - // 无语系列:ZTK C2016只能时间和电池图标变色。。。。 - if (QMUIDeviceHelper.isZTKC2016()) { - return false; - } - if (mStatusBarType != STATUSBAR_TYPE_DEFAULT) { + if (mStatusBarType != StatusBarType.Default) { return setStatusBarLightMode(activity, mStatusBarType); } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - if (isMIUICustomStatusBarLightModeImpl() && MIUISetStatusBarLightMode(activity.getWindow(), true)) { - mStatusBarType = STATUSBAR_TYPE_MIUI; - return true; - } else if (FlymeSetStatusBarLightMode(activity.getWindow(), true)) { - mStatusBarType = STATUSBAR_TYPE_FLYME; - return true; - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - Android6SetStatusBarLightMode(activity.getWindow(), true); - mStatusBarType = STATUSBAR_TYPE_ANDROID6; - return true; - } + 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; } @@ -200,12 +201,12 @@ public static boolean setStatusBarLightMode(Activity activity) { * @param activity 需要被处理的 Activity * @param type StatusBar 类型,对应不同的系统 */ - private static boolean setStatusBarLightMode(Activity activity, @StatusBarType int type) { - if (type == STATUSBAR_TYPE_MIUI) { + private static boolean setStatusBarLightMode(Activity activity, StatusBarType type) { + if (type == StatusBarType.Miui) { return MIUISetStatusBarLightMode(activity.getWindow(), true); - } else if (type == STATUSBAR_TYPE_FLYME) { + } else if (type == StatusBarType.Flyme) { return FlymeSetStatusBarLightMode(activity.getWindow(), true); - } else if (type == STATUSBAR_TYPE_ANDROID6) { + } else if (type == StatusBarType.Android6) { return Android6SetStatusBarLightMode(activity.getWindow(), true); } return false; @@ -218,41 +219,21 @@ private static boolean setStatusBarLightMode(Activity activity, @StatusBarType i */ public static boolean setStatusBarDarkMode(Activity activity) { if (activity == null) return false; - if (mStatusBarType == STATUSBAR_TYPE_DEFAULT) { + if (mStatusBarType == StatusBarType.Default) { // 默认状态,不需要处理 return true; } - if (mStatusBarType == STATUSBAR_TYPE_MIUI) { + if (mStatusBarType == StatusBarType.Miui) { return MIUISetStatusBarLightMode(activity.getWindow(), false); - } else if (mStatusBarType == STATUSBAR_TYPE_FLYME) { + } else if (mStatusBarType == StatusBarType.Flyme) { return FlymeSetStatusBarLightMode(activity.getWindow(), false); - } else if (mStatusBarType == STATUSBAR_TYPE_ANDROID6) { + } else if (mStatusBarType == StatusBarType.Android6) { return Android6SetStatusBarLightMode(activity.getWindow(), false); } return true; } - @TargetApi(23) - private static int changeStatusBarModeRetainFlag(Window window, int out) { - out = retainSystemUiFlag(window, out, View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); - out = retainSystemUiFlag(window, out, View.SYSTEM_UI_FLAG_FULLSCREEN); - out = retainSystemUiFlag(window, out, View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); - out = retainSystemUiFlag(window, out, View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); - out = retainSystemUiFlag(window, out, View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); - out = retainSystemUiFlag(window, out, View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); - return out; - } - - public static int retainSystemUiFlag(Window window, int out, int type) { - int now = window.getDecorView().getSystemUiVisibility(); - if ((now & type) == type) { - out |= type; - } - return out; - } - - /** * 设置状态栏字体图标为深色,Android 6 * @@ -262,10 +243,23 @@ public static int retainSystemUiFlag(Window window, int out, int type) { */ @TargetApi(23) private static boolean Android6SetStatusBarLightMode(Window window, boolean light) { - View decorView = window.getDecorView(); - int systemUi = light ? View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR : View.SYSTEM_UI_FLAG_LAYOUT_STABLE; - systemUi = changeStatusBarModeRetainFlag(window, systemUi); - decorView.setSystemUiVisibility(systemUi); + 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 @@ -333,7 +327,7 @@ public static boolean FlymeSetStatusBarLightMode(Window window, boolean light) { // flyme 在 6.2.0.0A 支持了 Android 官方的实现方案,旧的方案失效 // 高版本调用这个出现不可预期的 Bug,官方文档也没有给出完整的高低版本兼容方案 - if(QMUIDeviceHelper.isFlymeLowerThan(7)){ + if (QMUIDeviceHelper.isFlymeLowerThan(7)) { try { WindowManager.LayoutParams lp = window.getAttributes(); Field darkFlag = WindowManager.LayoutParams.class @@ -355,7 +349,7 @@ public static boolean FlymeSetStatusBarLightMode(Window window, boolean light) { } catch (Exception ignored) { } - }else if(QMUIDeviceHelper.isFlyme()){ + } else if (QMUIDeviceHelper.isFlyme()) { result = true; } } @@ -412,13 +406,6 @@ public static Integer getStatusBarAPITransparentValue(Context context) { return sTransparentValue; } - /** - * 检测 Android 6.0 是否可以启用 window.setStatusBarColor(Color.TRANSPARENT)。 - */ - public static boolean supportTransclentStatusBar6() { - return !(QMUIDeviceHelper.isZUKZ1() || QMUIDeviceHelper.isZTKC2016()); - } - /** * 获取状态栏的高度。 */ @@ -457,17 +444,11 @@ private static void initStatusBarHeight(Context context) { t.printStackTrace(); } } - if (QMUIDeviceHelper.isTablet(context) - && sStatusBarHeight > QMUIDisplayHelper.dp2px(context, STATUS_BAR_DEFAULT_HEIGHT_DP)) { - //状态栏高度大于25dp的平板,状态栏通常在下方 - sStatusBarHeight = 0; - } else { - 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); - } + 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); } } } @@ -479,10 +460,4 @@ public static void setVirtualDensity(float density) { public static void setVirtualDensityDpi(float densityDpi) { sVirtualDensityDpi = densityDpi; } - - @IntDef({STATUSBAR_TYPE_DEFAULT, STATUSBAR_TYPE_MIUI, STATUSBAR_TYPE_FLYME, STATUSBAR_TYPE_ANDROID6}) - @Retention(RetentionPolicy.SOURCE) - private @interface StatusBarType { - } - } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/util/QMUIToastHelper.java b/qmui/src/main/java/com/qmuiteam/qmui/util/QMUIToastHelper.java new file mode 100644 index 000000000..450925803 --- /dev/null +++ b/qmui/src/main/java/com/qmuiteam/qmui/util/QMUIToastHelper.java @@ -0,0 +1,98 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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 + } + } + } +} diff --git a/qmui/src/main/java/com/qmuiteam/qmui/util/QMUIViewHelper.java b/qmui/src/main/java/com/qmuiteam/qmui/util/QMUIViewHelper.java index f87bbad7b..30d6d8f46 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/util/QMUIViewHelper.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/util/QMUIViewHelper.java @@ -24,7 +24,6 @@ import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; -import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.ColorFilter; @@ -35,17 +34,10 @@ import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Build; -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; -import androidx.core.widget.ImageViewCompat; - import android.view.TouchDelegate; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; -import android.view.ViewStub; import android.view.Window; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; @@ -54,6 +46,12 @@ 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; @@ -587,19 +585,38 @@ public static void setPaddingBottom(View view, int value) { } } - /** - * 判断是否需要对 LineSpacingExtra 进行额外的兼容处理 - * 安卓 5.0 以下版本中,LineSpacingExtra 在最后一行也会产生作用,因此会多出一个 LineSpacingExtra 的空白,可以通过该方法判断后进行兼容处理 - * if (QMUIViewHelper.getISLastLineSpacingExtraError()) { - * textView.bottomMargin = -3dp; - * } else { - * textView.bottomMargin = 0; - * } - */ - public static boolean getIsLastLineSpacingExtraError() { - return Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP; + 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 会拦截这个事件 @@ -621,56 +638,6 @@ public static void safeRequestDisallowInterceptTouchEvent(@NonNull View view, bo } } - /** - * 把 ViewStub inflate 之后在其中根据 id 找 View - * - * @param parentView 包含 ViewStub 的 View - * @param viewStubId 要从哪个 ViewStub 来 inflate - * @param inflatedViewId 最终要找到的 View 的 id - * @return id 为 inflatedViewId 的 View - */ - public static View findViewFromViewStub(View parentView, int viewStubId, int inflatedViewId) { - if (null == parentView) { - return null; - } - View view = parentView.findViewById(inflatedViewId); - if (null == view) { - ViewStub vs = (ViewStub) parentView.findViewById(viewStubId); - if (null == vs) { - return null; - } - view = vs.inflate(); - if (null != view) { - view = view.findViewById(inflatedViewId); - } - } - return view; - } - - /** - * inflate ViewStub 并返回对应的 View。 - */ - public static View findViewFromViewStub(View parentView, int viewStubId, int inflatedViewId, int inflateLayoutResId) { - if (null == parentView) { - return null; - } - View view = parentView.findViewById(inflatedViewId); - if (null == view) { - ViewStub vs = (ViewStub) parentView.findViewById(viewStubId); - if (null == vs) { - return null; - } - if (vs.getLayoutResource() < 1 && inflateLayoutResId > 0) { - vs.setLayoutResource(inflateLayoutResId); - } - view = vs.inflate(); - if (null != view) { - view = view.findViewById(inflatedViewId); - } - } - return view; - } - public static void safeSetImageViewSelected(ImageView imageView, boolean selected) { // imageView setSelected 实现有问题。 // resizeFromDrawable 中判断 drawable size 是否改变而调用 requestLayout,看似合理,但不会被调用 @@ -733,6 +700,33 @@ public static void getDescendantRect(ViewGroup parent, View descendant, Rect out 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<Matrix> sMatrix = new ThreadLocal<>(); @@ -747,6 +741,7 @@ public static void offsetDescendantRect(ViewGroup group, View child, Rect rect) m.reset(); } + m.preTranslate(-group.getScrollX(), -group.getScrollY()); offsetDescendantMatrix(group, child, m); RectF rectF = sRectF.get(); diff --git a/qmui/src/main/java/com/qmuiteam/qmui/util/QMUIWindowHelper.java b/qmui/src/main/java/com/qmuiteam/qmui/util/QMUIWindowHelper.java index 517b05f6e..6b1e00668 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/util/QMUIWindowHelper.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/util/QMUIWindowHelper.java @@ -22,18 +22,22 @@ import android.view.ViewParent; import android.view.WindowManager; -import com.qmuiteam.qmui.BuildConfig; - -import java.lang.reflect.Field; - 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 * <p> @@ -46,11 +50,7 @@ public class QMUIWindowHelper { */ public static void setWindowType(WindowManager.LayoutParams layoutParams) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - layoutParams.type = WindowManager.LayoutParams.TYPE_TOAST; - } else { - layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE; - } + layoutParams.type = WindowManager.LayoutParams.TYPE_TOAST; } @@ -58,7 +58,7 @@ public static void setWindowType(WindowManager.LayoutParams layoutParams) { @SuppressWarnings({"JavaReflectionMemberAccess"}) public static Rect unSafeGetWindowVisibleInsets(@NonNull View view) { Object attachInfo = getAttachInfoFromView(view); - if(attachInfo == null){ + if (attachInfo == null) { return null; } try { @@ -70,7 +70,7 @@ public static Rect unSafeGetWindowVisibleInsets(@NonNull View view) { return (Rect) visibleInsets; } } catch (Throwable e) { - if (BuildConfig.DEBUG) { + if (QMUIConfig.DEBUG) { e.printStackTrace(); } } @@ -81,7 +81,7 @@ public static Rect unSafeGetWindowVisibleInsets(@NonNull View view) { @SuppressWarnings({"JavaReflectionMemberAccess"}) public static Rect unSafeGetContentInsets(@NonNull View view) { Object attachInfo = getAttachInfoFromView(view); - if(attachInfo == null){ + if (attachInfo == null) { return null; } try { @@ -93,34 +93,34 @@ public static Rect unSafeGetContentInsets(@NonNull View view) { return (Rect) visibleInsets; } } catch (Throwable e) { - if (BuildConfig.DEBUG) { + if (QMUIConfig.DEBUG) { e.printStackTrace(); } } return null; } - public static Object getAttachInfoFromView(@NonNull View view){ + public static Object getAttachInfoFromView(@NonNull View view) { Object attachInfo = null; - if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P){ + 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){ + if (rootView != null) { ViewParent vp = rootView.getParent(); - if(vp != null){ + if (vp != null) { try { Field field = vp.getClass().getDeclaredField("mAttachInfo"); field.setAccessible(true); attachInfo = field.get(vp); } catch (Throwable e) { - if (BuildConfig.DEBUG) { + if (QMUIConfig.DEBUG) { e.printStackTrace(); } } } } - }else{ + } else { try { // Android P forbid the reflection for @hide filed, // fortunately now it is in light greylist, just be warned. @@ -128,7 +128,7 @@ public static Object getAttachInfoFromView(@NonNull View view){ field.setAccessible(true); attachInfo = field.get(view); } catch (Throwable e) { - if (BuildConfig.DEBUG) { + if (QMUIConfig.DEBUG) { e.printStackTrace(); } } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/util/QMUIWindowInsetHelper.java b/qmui/src/main/java/com/qmuiteam/qmui/util/QMUIWindowInsetHelper.java index 0dce85832..22bd942eb 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/util/QMUIWindowInsetHelper.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/util/QMUIWindowInsetHelper.java @@ -16,34 +16,23 @@ package com.qmuiteam.qmui.util; -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.content.res.Configuration; -import android.graphics.Rect; import android.os.Build; -import android.view.DisplayCutout; 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 com.qmuiteam.qmui.R; -import com.qmuiteam.qmui.widget.INotchInsetConsumer; -import com.qmuiteam.qmui.widget.IWindowInsetLayout; -import com.qmuiteam.qmui.widget.IWindowInsetKeyboardConsumer; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; - import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.constraintlayout.widget.ConstraintLayout; -import androidx.coordinatorlayout.widget.CoordinatorLayout; +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 androidx.drawerlayout.widget.DrawerLayout; + +import com.qmuiteam.qmui.R; /** * @author cginechen @@ -51,352 +40,263 @@ */ public class QMUIWindowInsetHelper { - public static final int KEYBOARD_HEIGHT_BOUNDARY_DP = 100; - private static final Object KEYBOARD_CONSUMER = new Object(); - private static ArrayList<Class<? extends ViewGroup>> sCustomHandlerContainerList = new ArrayList<>(); - private final int KEYBOARD_HEIGHT_BOUNDARY; - private final WeakReference<IWindowInsetLayout> mWindowInsetLayoutWR; - private int sApplySystemWindowInsetsCount = 0; - - public QMUIWindowInsetHelper(ViewGroup viewGroup, IWindowInsetLayout windowInsetLayout) { - mWindowInsetLayoutWR = new WeakReference<>(windowInsetLayout); - KEYBOARD_HEIGHT_BOUNDARY = QMUIDisplayHelper.dp2px(viewGroup.getContext(), KEYBOARD_HEIGHT_BOUNDARY_DP); - - if (QMUINotchHelper.isNotchOfficialSupport()) { - setOnApplyWindowInsetsListener28(viewGroup); - } else { - // some rom crash with WindowInsets... - ViewCompat.setOnApplyWindowInsetsListener(viewGroup, - new OnApplyWindowInsetsListener() { - @Override - public WindowInsetsCompat onApplyWindowInsets(View v, - WindowInsetsCompat insets) { - if (Build.VERSION.SDK_INT >= 21 && mWindowInsetLayoutWR.get() != null) { - if (mWindowInsetLayoutWR.get().applySystemWindowInsets21(insets)) { - if(insets.isConsumed()){ - return insets; - } - insets = insets.consumeSystemWindowInsets(); - if(insets.isConsumed()){ - return insets; - } - return insets.consumeStableInsets(); - } - } - return insets; - } - }); - } - } - @TargetApi(28) - private void setOnApplyWindowInsetsListener28(ViewGroup viewGroup) { - // WindowInsetsCompat does not exist DisplayCutout stuff... - viewGroup.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() { - @Override - public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) { - if (mWindowInsetLayoutWR.get() != null && - mWindowInsetLayoutWR.get().applySystemWindowInsets21(windowInsets)) { - windowInsets = windowInsets.consumeSystemWindowInsets(); - - DisplayCutout displayCutout = windowInsets.getDisplayCutout(); - if (displayCutout != null) { - windowInsets = windowInsets.consumeDisplayCutout(); - } - if(windowInsets.isConsumed()){ - return windowInsets; - } - return windowInsets.consumeStableInsets(); - } - return windowInsets; - } - }); - } + 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); + } + }; - @SuppressWarnings("deprecation") - @TargetApi(19) - public boolean defaultApplySystemWindowInsets19(ViewGroup viewGroup, Rect insets) { - boolean consumed = false; - if (insets.bottom >= KEYBOARD_HEIGHT_BOUNDARY && shouldInterceptKeyboardInset(viewGroup)) { - if(viewGroup instanceof IWindowInsetKeyboardConsumer){ - ((IWindowInsetKeyboardConsumer)viewGroup).onHandleKeyboard(insets.bottom); - }else{ - QMUIViewHelper.setPaddingBottom(viewGroup, insets.bottom); - } - viewGroup.setTag(R.id.qmui_window_inset_keyboard_area_consumer, KEYBOARD_CONSUMER); - insets.bottom = 0; - } else { - viewGroup.setTag(R.id.qmui_window_inset_keyboard_area_consumer, null); - if(viewGroup instanceof IWindowInsetKeyboardConsumer){ - ((IWindowInsetKeyboardConsumer)viewGroup).onHandleKeyboard(0); - }else{ - QMUIViewHelper.setPaddingBottom(viewGroup, 0); - } + public final static InsetHandler consumeInsetWithPaddingIgnoreBottomHandler = new InsetHandler() { + @Override + public void handleInset(View view, Insets insets) { + view.setPadding(insets.left, insets.top, insets.right, 0); } + }; - for (int i = 0; i < viewGroup.getChildCount(); i++) { - View child = viewGroup.getChildAt(i); - if (jumpDispatch(child)) { - continue; - } + public final static InsetHandler consumeInsetWithPaddingIgnoreTopHandler = new InsetHandler() { + @Override + public void handleInset(View view, Insets insets) { + view.setPadding(insets.left, 0, insets.right, insets.bottom); + } + }; - Rect childInsets = new Rect(insets); - computeInsets(child, childInsets); - - if (!isHandleContainer(child)) { - child.setPadding(childInsets.left, childInsets.top, childInsets.right, childInsets.bottom); - } else { - if (child instanceof IWindowInsetLayout) { - boolean output = ((IWindowInsetLayout) child).applySystemWindowInsets19(childInsets); - consumed = consumed || output; - } else { - boolean output = defaultApplySystemWindowInsets19((ViewGroup) child, childInsets); - consumed = consumed || output; - } - } + 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); } + }; - return consumed; - } + private final static OnApplyWindowInsetsListener sStopDispatchListener = new OnApplyWindowInsetsListener() { + @Override + public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) { + return WindowInsetsCompat.CONSUMED; + } + }; - @TargetApi(21) - public boolean defaultApplySystemWindowInsets21(ViewGroup viewGroup, Object insets) { - if (QMUINotchHelper.isNotchOfficialSupport()) { - return defaultApplySystemWindowInsets(viewGroup, (WindowInsets) insets); - } else { - return defaultApplySystemWindowInsetsCompat(viewGroup, (WindowInsetsCompat) insets); + 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); } - @TargetApi(21) - public boolean defaultApplySystemWindowInsetsCompat(ViewGroup viewGroup, WindowInsetsCompat insets) { - boolean consumed = false; - boolean showKeyboard = false; - if (insets.getSystemWindowInsetBottom() >= KEYBOARD_HEIGHT_BOUNDARY && - shouldInterceptKeyboardInset(viewGroup)) { - showKeyboard = true; - if(viewGroup instanceof IWindowInsetKeyboardConsumer){ - ((IWindowInsetKeyboardConsumer)viewGroup).onHandleKeyboard(insets.getSystemWindowInsetBottom()); - }else{ - QMUIViewHelper.setPaddingBottom(viewGroup, insets.getSystemWindowInsetBottom()); - } - viewGroup.setTag(R.id.qmui_window_inset_keyboard_area_consumer, KEYBOARD_CONSUMER); - } else { - if(viewGroup instanceof IWindowInsetKeyboardConsumer){ - ((IWindowInsetKeyboardConsumer)viewGroup).onHandleKeyboard(0); - }else{ - QMUIViewHelper.setPaddingBottom(viewGroup, 0); - } - viewGroup.setTag(R.id.qmui_window_inset_keyboard_area_consumer, null); - } + public static void handleWindowInsets(View v, @WindowInsetsCompat.Type.InsetsType final int insetsType, boolean jumpSelfHandleIfMatchLast){ + handleWindowInsets(v, insetsType, jumpSelfHandleIfMatchLast, false); + } - for (int i = 0; i < viewGroup.getChildCount(); i++) { - View child = viewGroup.getChildAt(i); + public static void handleWindowInsets(View v, @WindowInsetsCompat.Type.InsetsType final int insetsType, boolean jumpSelfHandleIfMatchLast, boolean ignoreVisibility){ + handleWindowInsets(v, insetsType, consumeInsetWithPaddingWithGravityHandler, jumpSelfHandleIfMatchLast, ignoreVisibility, false); + } - if (jumpDispatch(child)) { - continue; - } + 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); + } - int insetLeft = insets.getSystemWindowInsetLeft(); - int insetRight = insets.getSystemWindowInsetRight(); - if (QMUINotchHelper.needFixLandscapeNotchAreaFitSystemWindow(viewGroup) && - viewGroup.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { - insetLeft = Math.max(insetLeft, QMUINotchHelper.getSafeInsetLeft(viewGroup)); - insetRight = Math.max(insetRight, QMUINotchHelper.getSafeInsetRight(viewGroup)); + /** + * + * @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); + } - Rect childInsets = new Rect( - insetLeft, - insets.getSystemWindowInsetTop(), - insetRight, - showKeyboard ? 0 : insets.getSystemWindowInsetBottom()); - - computeInsets(child, childInsets); - WindowInsetsCompat windowInsetsCompat = ViewCompat.dispatchApplyWindowInsets(child, insets.replaceSystemWindowInsets(childInsets)); - consumed = consumed || (windowInsetsCompat != null && windowInsetsCompat.isConsumed()); - } + /** + * 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); + } - return consumed; + public static void overrideWithDoNotHandleWindowInsets(View v){ + setOnApplyWindowInsetsListener(v, sOverrideWithNothingHandleListener, false); } - @TargetApi(28) - public boolean defaultApplySystemWindowInsets(ViewGroup viewGroup, WindowInsets insets) { - sApplySystemWindowInsetsCount++; - if (QMUINotchHelper.isNotchOfficialSupport()) { - if (sApplySystemWindowInsetsCount == 1) { - // avoid dispatching multiple times - dispatchNotchInsetChange(viewGroup); - } - // always consume display cutout!! - insets = insets.consumeDisplayCutout(); + // 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); } - boolean consumed = false; - boolean showKeyboard = false; - if (insets.getSystemWindowInsetBottom() >= KEYBOARD_HEIGHT_BOUNDARY && - shouldInterceptKeyboardInset(viewGroup)) { - showKeyboard = true; - if(viewGroup instanceof IWindowInsetKeyboardConsumer){ - ((IWindowInsetKeyboardConsumer)viewGroup).onHandleKeyboard(insets.getSystemWindowInsetBottom()); - }else{ - QMUIViewHelper.setPaddingBottom(viewGroup, insets.getSystemWindowInsetBottom()); - } - viewGroup.setTag(R.id.qmui_window_inset_keyboard_area_consumer, KEYBOARD_CONSUMER); - } else { - if(viewGroup instanceof IWindowInsetKeyboardConsumer){ - ((IWindowInsetKeyboardConsumer)viewGroup).onHandleKeyboard(0); - }else{ - QMUIViewHelper.setPaddingBottom(viewGroup, 0); - } - viewGroup.setTag(R.id.qmui_window_inset_keyboard_area_consumer, null); + 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; } - for (int i = 0; i < viewGroup.getChildCount(); i++) { - View child = viewGroup.getChildAt(i); - if (jumpDispatch(child)) { - continue; - } + v.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() { + WindowInsetsCompat mLastInsets = null; + WindowInsets mReturnedInsets = null; - Rect childInsets = new Rect( - insets.getSystemWindowInsetLeft(), - insets.getSystemWindowInsetTop(), - insets.getSystemWindowInsetRight(), - showKeyboard ? 0 : insets.getSystemWindowInsetBottom()); - computeInsets(child, childInsets); - WindowInsets childWindowInsets = insets.replaceSystemWindowInsets(childInsets); - WindowInsets windowInsets = child.dispatchApplyWindowInsets(childWindowInsets); - consumed = consumed || windowInsets.isConsumed(); - } - sApplySystemWindowInsetsCount--; - return consumed; - } + @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); - private boolean shouldInterceptKeyboardInset(ViewGroup viewGroup){ - return viewGroup.getClass().getAnnotation(DoNotInterceptKeyboardInset.class) == null; - } + if (Build.VERSION.SDK_INT >= 30) { + return compatInsets.toWindowInsets(); + } - private void dispatchNotchInsetChange(View view) { - if (view instanceof INotchInsetConsumer) { - boolean stop = ((INotchInsetConsumer) view).notifyInsetMaybeChanged(); - if (stop) { - return; - } - } - if (view instanceof ViewGroup) { - ViewGroup viewGroup = (ViewGroup) view; - int childCount = viewGroup.getChildCount(); - for (int i = 0; i < childCount; i++) { - dispatchNotchInsetChange(viewGroup.getChildAt(i)); - } - } - } + // 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); + } - @SuppressWarnings("deprecation") - @TargetApi(19) - public static boolean jumpDispatch(View child) { - return !child.getFitsSystemWindows() && !isHandleContainer(child); + // 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; + } + }); } - public static boolean isHandleContainer(View child) { - boolean ret = child instanceof IWindowInsetLayout || - child instanceof CoordinatorLayout || - child instanceof DrawerLayout; - if (ret) { - return true; - } - for (Class<? extends View> clz : sCustomHandlerContainerList) { - if (clz.isInstance(child)) { - return true; - } + /** + * 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); } - return false; } - public static void addHandleContainer(Class<? extends ViewGroup> clazz) { - sCustomHandlerContainerList.add(clazz); - } + public static Insets adapterInsetsWithGravity(View view, Insets insets){ + int left = insets.left; + int right = insets.right; + int top = insets.top; + int bottom = insets.bottom; - public void computeInsets(View view, Rect insets) { ViewGroup.LayoutParams lp = view.getLayoutParams(); if(lp instanceof ConstraintLayout.LayoutParams){ - computeInsetsWithConstraint(view, insets, (ConstraintLayout.LayoutParams) lp); - }else{ - computeInsetsWithGravity(view, insets, lp); - } - } - - @SuppressLint("RtlHardcoded") - public void computeInsetsWithGravity(View view, Rect insets, ViewGroup.LayoutParams lp) { - 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: - insets.right = 0; - break; - case Gravity.RIGHT: - insets.left = 0; - break; + 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 (lp.height != ViewGroup.LayoutParams.MATCH_PARENT) { - int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; - switch (verticalGravity) { - case Gravity.TOP: - insets.bottom = 0; - break; - case Gravity.BOTTOM: - insets.top = 0; - break; + 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; } - } - } - public void computeInsetsWithConstraint(View view, Rect insets, ConstraintLayout.LayoutParams lp){ - if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) { - if(lp.leftToLeft == ConstraintLayout.LayoutParams.PARENT_ID){ - insets.right = 0; - }else if(lp.rightToRight == ConstraintLayout.LayoutParams.PARENT_ID){ - insets.left = 0; + 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.WRAP_CONTENT) { - if(lp.topToTop == ConstraintLayout.LayoutParams.PARENT_ID){ - insets.bottom = 0; - }else if(lp.bottomToBottom == ConstraintLayout.LayoutParams.PARENT_ID){ - insets.top = 0; + 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 static View findKeyboardAreaConsumer(@NonNull View view) { - while (view != null) { - Object tag = view.getTag(R.id.qmui_window_inset_keyboard_area_consumer); - if (KEYBOARD_CONSUMER == tag) { - return view; - } - ViewParent viewParent = view.getParent(); - if (viewParent instanceof View) { - view = (View) viewParent; - } else { - view = null; - } - } - return null; + public interface InsetHandler{ + void handleInset(View view, Insets insets); } } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/IWindowInsetLayout.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/IWindowInsetLayout.java deleted file mode 100644 index 0182fae3a..000000000 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/IWindowInsetLayout.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.graphics.Rect; - -/** - * @author cginechen - * @date 2017-09-13 - */ - -public interface IWindowInsetLayout { - boolean applySystemWindowInsets19(Rect insets); - - boolean applySystemWindowInsets21(Object insets); -} diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIAnimationListView.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIAnimationListView.java index fe720d1d8..db7fd8776 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIAnimationListView.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIAnimationListView.java @@ -21,15 +21,11 @@ import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; -import android.annotation.TargetApi; import android.content.Context; import android.database.DataSetObserver; import android.graphics.Canvas; -import android.os.Build; import android.os.Looper; import android.os.SystemClock; -import androidx.collection.LongSparseArray; -import androidx.core.view.ViewCompat; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; @@ -42,6 +38,9 @@ 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; @@ -116,7 +115,6 @@ public QMUIAnimationListView(Context context, AttributeSet attrs, int defStyleAt init(); } - @TargetApi(Build.VERSION_CODES.LOLLIPOP) public QMUIAnimationListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(); diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIAppBarLayout.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIAppBarLayout.java index 71306200b..1f4135fff 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIAppBarLayout.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIAppBarLayout.java @@ -17,31 +17,13 @@ package com.qmuiteam.qmui.widget; import android.content.Context; -import android.graphics.Rect; -import com.google.android.material.appbar.AppBarLayout; -import androidx.core.view.ViewCompat; -import androidx.core.view.WindowInsetsCompat; import android.util.AttributeSet; -import android.view.View; - -import com.qmuiteam.qmui.util.QMUIWindowInsetHelper; -import java.lang.reflect.Field; - -import androidx.coordinatorlayout.widget.CoordinatorLayout; +import com.google.android.material.appbar.AppBarLayout; -/** - * add support for API 19 when use with {@link CoordinatorLayout} - * and {@link QMUICollapsingTopBarLayout} - * <p> - * notice: we use reflection to change the field value in AppBarLayout. use it only if you need to - * set fitSystemWindows for StatusBar - * - * @author cginechen - * @date 2017-09-20 - */ +@Deprecated +public class QMUIAppBarLayout extends AppBarLayout { -public class QMUIAppBarLayout extends AppBarLayout implements IWindowInsetLayout { public QMUIAppBarLayout(Context context) { super(context); } @@ -50,57 +32,4 @@ public QMUIAppBarLayout(Context context, AttributeSet attrs) { super(context, attrs); } - @Override - public boolean applySystemWindowInsets19(final Rect insets) { - if (ViewCompat.getFitsSystemWindows(this)) { - Field field = null; - try { - // support 28 change the name - field = AppBarLayout.class.getDeclaredField("lastInsets"); - } catch (NoSuchFieldException e) { - try { - field = AppBarLayout.class.getDeclaredField("mLastInsets"); - } catch (NoSuchFieldException ignored) { - - } - } - - if (field != null) { - field.setAccessible(true); - try { - field.set(this, new WindowInsetsCompat(null) { - @Override - public int getSystemWindowInsetTop() { - return insets.top; - } - }); - } catch (IllegalAccessException ignored) { - - } - } - - for (int i = 0; i < getChildCount(); i++) { - View child = getChildAt(i); - if (QMUIWindowInsetHelper.jumpDispatch(child)) { - continue; - } - - - if (!QMUIWindowInsetHelper.isHandleContainer(child)) { - child.setPadding(insets.left, insets.top, insets.right, insets.bottom); - } else { - if (child instanceof IWindowInsetLayout) { - ((IWindowInsetLayout) child).applySystemWindowInsets19(insets); - } - } - } - return true; - } - return false; - } - - @Override - public boolean applySystemWindowInsets21(Object insets) { - return true; - } } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUICollapsingTopBarLayout.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUICollapsingTopBarLayout.java index 42d278172..52a441221 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUICollapsingTopBarLayout.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUICollapsingTopBarLayout.java @@ -43,13 +43,13 @@ import android.graphics.Typeface; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; -import android.os.Build; 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; @@ -62,6 +62,7 @@ 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; @@ -78,12 +79,14 @@ 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; @@ -94,7 +97,7 @@ * @date 2017-09-02 */ -public class QMUICollapsingTopBarLayout extends FrameLayout implements IWindowInsetLayout, IQMUISkinDispatchInterceptor { +public class QMUICollapsingTopBarLayout extends FrameLayout implements IQMUISkinDispatchInterceptor { private static final int DEFAULT_SCRIM_ANIMATION_DURATION = 600; @@ -126,7 +129,7 @@ public class QMUICollapsingTopBarLayout extends FrameLayout implements IWindowIn int mCurrentOffset; - Object mLastInsets; + Insets mLastInsets; private int mContentScrimSkinAttr = 0; private int mStatusBarScrimSkinAttr = 0; @@ -217,22 +220,28 @@ public QMUICollapsingTopBarLayout(Context context, AttributeSet attrs, int defSt setWillNotDraw(false); - ViewCompat.setOnApplyWindowInsetsListener(this, - new androidx.core.view.OnApplyWindowInsetsListener() { + QMUIWindowInsetHelper.handleWindowInsets(this, + WindowInsetsCompat.Type.statusBars() | WindowInsetsCompat.Type.displayCutout(), + new QMUIWindowInsetHelper.InsetHandler() { @Override - public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) { - return setWindowInsets(insets); + 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(); + } } - }); - } - - private WindowInsetsCompat setWindowInsets(WindowInsetsCompat insets) { - if (Build.VERSION.SDK_INT >= 21) { - if (applySystemWindowInsets21(insets)) { - return insets.consumeSystemWindowInsets(); - } - } - return insets; + }, + true, + false, + true + ); } public void followTopBarCommonSkin() { @@ -313,11 +322,7 @@ public void draw(Canvas canvas) { private int getWindowInsetTop() { if (mLastInsets != null) { - if (mLastInsets instanceof WindowInsetsCompat) { - return ((WindowInsetsCompat) mLastInsets).getSystemWindowInsetTop(); - } else if (mLastInsets instanceof Rect) { - return ((Rect) mLastInsets).top; - } + return mLastInsets.top; } return 0; } @@ -404,6 +409,13 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 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); @@ -1118,46 +1130,6 @@ protected FrameLayout.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p return new QMUICollapsingTopBarLayout.LayoutParams(p); } - @Override - @SuppressWarnings("deprecation") - protected boolean fitSystemWindows(Rect insets) { - return applySystemWindowInsets19(insets); - } - - @Override - public boolean applySystemWindowInsets19(Rect insets) { - Rect newInsets = null; - if (ViewCompat.getFitsSystemWindows(this)) { - // 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 (!QMUILangHelper.objectEquals(mLastInsets, newInsets)) { - mLastInsets = newInsets; - requestLayout(); - } - - // Consume the insets. This is done so that child views with fitSystemWindows=true do not - // get the default padding functionality from View - return true; - } - - @Override - public boolean applySystemWindowInsets21(Object insets) { - Object newInsets = null; - if (ViewCompat.getFitsSystemWindows(this)) { - // 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 (!QMUILangHelper.objectEquals(mLastInsets, newInsets)) { - mLastInsets = newInsets; - requestLayout(); - } - return true; - } public static class LayoutParams extends FrameLayout.LayoutParams { @@ -1170,7 +1142,7 @@ public static class LayoutParams extends FrameLayout.LayoutParams { COLLAPSE_MODE_PARALLAX }) @Retention(RetentionPolicy.SOURCE) - @interface CollapseMode { + public @interface CollapseMode { } /** diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIItemViewsAdapter.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIItemViewsAdapter.java index d782e3663..073dfdb82 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIItemViewsAdapter.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIItemViewsAdapter.java @@ -19,14 +19,14 @@ 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; -import androidx.core.util.Pools; -import androidx.recyclerview.widget.RecyclerView; - /** * 一个带 cache 功能的“列表型数据-View”的适配器,适用于自定义 {@link View} 需要显示重复单元 {@link android.widget.ListView} 的情景, * cache 功能主要是保证在需要多次刷新数据或布局的情况下({@link android.widget.ListView} 或 {@link RecyclerView} 的 itemView) @@ -60,6 +60,7 @@ public void detach(int count) { 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) { } @@ -86,6 +87,10 @@ private V getView() { protected abstract V createView(ViewGroup parentView); + protected void onViewRecycled(V v){ + + } + public QMUIItemViewsAdapter<T, V> addItem(T item) { mItemData.add(item); return this; diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUINotchConsumeLayout.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUINotchConsumeLayout.java index 6a46ffb07..54d65a4b4 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUINotchConsumeLayout.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUINotchConsumeLayout.java @@ -19,11 +19,15 @@ 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 implements INotchInsetConsumer { +public class QMUINotchConsumeLayout extends FrameLayout { public QMUINotchConsumeLayout(Context context) { this(context, null); } @@ -34,7 +38,13 @@ public QMUINotchConsumeLayout(Context context, AttributeSet attrs) { public QMUINotchConsumeLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - setFitsSystemWindows(false); + QMUIWindowInsetHelper.setOnApplyWindowInsetsListener(this, new androidx.core.view.OnApplyWindowInsetsListener() { + @Override + public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) { + notifyInsetMaybeChanged(); + return insets; + } + }, true); } @Override @@ -53,7 +63,6 @@ protected void onConfigurationChanged(Configuration newConfig) { } } - @Override public boolean notifyInsetMaybeChanged() { setPadding( QMUINotchHelper.getSafeInsetLeft(this), diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIPagerAdapter.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIPagerAdapter.java index 4418e3ff8..85b8d3553 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIPagerAdapter.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIPagerAdapter.java @@ -61,7 +61,6 @@ public Object instantiateItem(@NonNull ViewGroup container, int position) { @Override public final void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { destroy(container, position, object); - } /** diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIProgressBar.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIProgressBar.java index 1116fb456..01662ea15 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIProgressBar.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIProgressBar.java @@ -26,11 +26,11 @@ import android.util.AttributeSet; import android.view.View; +import androidx.core.view.ViewCompat; + import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.util.QMUIDisplayHelper; -import androidx.core.view.ViewCompat; - /** * 一个进度条控件,通过颜色变化显示进度,支持环形和矩形两种形式,主要特性如下: * <ol> @@ -45,8 +45,10 @@ public class QMUIProgressBar extends View { public final static int TYPE_RECT = 0; - public final static int TYPE_CIRCLE = 1; - public final static int TYPE_ROUND_RECT = 2; + public final static int 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; @@ -80,7 +82,7 @@ public class QMUIProgressBar extends View { private RectF mArcOval = new RectF(); private String mText = ""; private int mStrokeWidth; - private int mCircleRadius; + private float mCircleRadius; private Point mCenterPoint; private OnProgressChangeListener mOnProgressChangeListener; private Runnable mNotifyProgressChangeAction = new Runnable() { @@ -128,11 +130,11 @@ public void setup(Context context, AttributeSet attrs) { mTextColor = array.getColor(R.styleable.QMUIProgressBar_android_textColor, DEFAULT_TEXT_COLOR); } - if (mType == TYPE_CIRCLE) { + 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); + configPaint(mTextColor, mTextSize, mRoundCap, mStrokeWidth); setProgress(mValue); } @@ -141,31 +143,52 @@ public void setOnProgressChangeListener(OnProgressChangeListener onProgressChang 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) / 2; + 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) { + 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(mStrokeWidth); + 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(mStrokeWidth); + mBackgroundPaint.setStrokeWidth(strokeWidth); mBackgroundPaint.setAntiAlias(true); } mTextPaint.setColor(textColor); @@ -175,7 +198,7 @@ private void configPaint(int textColor, int textSize, boolean isRoundCap) { public void setType(int type) { mType = type; - configPaint(mTextColor, mTextSize, mRoundCap); + configPaint(mTextColor, mTextSize, mRoundCap, mStrokeWidth); invalidate(); } @@ -260,7 +283,7 @@ protected void onDraw(Canvas canvas) { mText = mQMUIProgressBarTextGenerator.generateText(this, mValue, mMaxValue); } if(((mType == TYPE_RECT || mType == TYPE_ROUND_RECT) && mBgRect == null) || - (mType == TYPE_CIRCLE && mCenterPoint == null)){ + ((mType == TYPE_CIRCLE || mType == TYPE_FILL_CIRCLE) && mCenterPoint == null)){ // npe protect, sometimes measure may not be called by parent. configShape(); } @@ -269,7 +292,7 @@ protected void onDraw(Canvas canvas) { } else if (mType == TYPE_ROUND_RECT) { drawRoundRect(canvas); } else { - drawCircle(canvas); + drawCircle(canvas, mType == TYPE_FILL_CIRCLE); } } @@ -306,14 +329,14 @@ private void drawRoundRect(Canvas canvas) { } } - private void drawCircle(Canvas canvas) { + 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, false, mPaint); + canvas.drawArc(mArcOval, 270, 360f * mValue / mMaxValue, useCenter, mPaint); } if (mText != null && mText.length() > 0) { Paint.FontMetricsInt fontMetrics = mTextPaint.getFontMetricsInt(); diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIRadiusImageView2.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIRadiusImageView2.java index 02c188a72..071b95ed6 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIRadiusImageView2.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIRadiusImageView2.java @@ -26,14 +26,14 @@ 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; -import androidx.annotation.ColorInt; -import androidx.appcompat.widget.AppCompatImageView; - /** * shown image in radius view, is different to {@link QMUIRadiusImageView} * the oval is not supported @@ -100,6 +100,9 @@ private void init(Context context, AttributeSet attrs, int defStyleAttr) { R.styleable.QMUIRadiusImageView2_qmui_corner_radius, 0)); } array.recycle(); + + mLayoutHelper.setBorderWidth(mBorderWidth); + mLayoutHelper.setBorderColor(mBorderColor); } @@ -491,7 +494,7 @@ protected void dispatchDraw(Canvas canvas) { @Override public void setSelected(boolean selected) { - if(!mIsInOnTouchEvent){ + if (!mIsInOnTouchEvent) { super.setSelected(selected); } if (mIsSelected != selected) { @@ -540,11 +543,10 @@ public void setColorFilter(ColorFilter cf) { @Override public boolean onTouchEvent(MotionEvent event) { - mIsInOnTouchEvent = true; if (!this.isClickable()) { - this.setSelected(false); return super.onTouchEvent(event); - }else if(mIsTouchSelectModeEnabled){ + } else if (mIsTouchSelectModeEnabled) { + mIsInOnTouchEvent = true; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: this.setSelected(true); @@ -556,8 +558,9 @@ public boolean onTouchEvent(MotionEvent event) { this.setSelected(false); break; } + mIsInOnTouchEvent = false; } - mIsInOnTouchEvent = false; + return super.onTouchEvent(event); } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUISlider.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUISlider.java index 2c2b302c1..4f55dc44c 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUISlider.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUISlider.java @@ -56,7 +56,9 @@ public class QMUISlider extends FrameLayout implements IQMUISkinDefaultAttrProvi 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; @@ -65,6 +67,7 @@ public class QMUISlider extends FrameLayout implements IQMUISkinDefaultAttrProvi private boolean mIsMoving = false; private int mTouchSlop; private RectF mTempRect = new RectF(); + private LongPressAction mLongPressAction = new LongPressAction(); private static SimpleArrayMap<String, Integer> sDefaultSkinAttrs; @@ -140,7 +143,8 @@ public void setCallback(Callback callback) { public void setCurrentProgress(int currentProgress) { if (!mIsMoving) { int progress = QMUILangHelper.constrain(currentProgress, 0, mTickCount); - if (mCurrentProgress != progress) { + if (mCurrentProgress != progress || !mIsProgressFirstSet) { + mIsProgressFirstSet = true; safeSetCurrentProgress(progress); if (mCallback != null) { mCallback.onProgressChange(this, progress, mTickCount, false); @@ -167,6 +171,8 @@ public int getCurrentProgress() { public void setTickCount(int tickCount) { if (mTickCount != tickCount) { mTickCount = tickCount; + setCurrentProgress(QMUILangHelper.constrain(mCurrentProgress, 0, mTickCount)); + mThumbView.render(mCurrentProgress, mTickCount); invalidate(); } } @@ -229,6 +235,9 @@ public boolean onTouchEvent(MotionEvent event) { mIsThumbTouched = isThumbTouched(event.getX(), event.getY()); if (mIsThumbTouched) { mThumbView.setPress(true); + }else if(mLongTouchToChangeProgress){ + removeCallbacks(mLongPressAction); + postOnAnimationDelayed(mLongPressAction, 300); } if (mCallback != null) { @@ -241,6 +250,7 @@ public boolean onTouchEvent(MotionEvent event) { mLastTouchX = x; if (!mIsMoving && mIsThumbTouched) { if (Math.abs(mLastTouchX - mDownTouchX) > mTouchSlop) { + removeCallbacks(mLongPressAction); mIsMoving = true; if (mCallback != null) { mCallback.onStartMoving(this, mCurrentProgress, mTickCount); @@ -267,7 +277,7 @@ public boolean onTouchEvent(MotionEvent event) { 0, maxOffset) ); - calculateByThumbPosition(); + calculateByThumbPosition(maxOffset); } if (mCallback != null && oldProgress != mCurrentProgress) { mCallback.onProgressChange(this, mCurrentProgress, mTickCount, true); @@ -276,6 +286,7 @@ public boolean onTouchEvent(MotionEvent event) { } } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + removeCallbacks(mLongPressAction); mLastTouchX = -1; QMUIViewHelper.safeRequestDisallowInterceptTouchEvent(this, false); if (mIsMoving) { @@ -308,6 +319,8 @@ public boolean onTouchEvent(MotionEvent event) { if (mCallback != null) { mCallback.onTouchUp(this, mCurrentProgress, mTickCount); } + } else { + removeCallbacks(mLongPressAction); } return true; @@ -326,7 +339,7 @@ private void checkTouch(int touchX, int maxOffset) { mThumbViewOffsetHelper.setLeftAndRightOffset(maxOffset); safeSetCurrentProgress(mTickCount); } else { - float percent = (float) moveX / (getWidth() - getPaddingLeft() - getPaddingLeft() - 2 * mThumbView.getLeftRightMargin()); + float percent = (float) moveX / (getWidth() - getPaddingLeft() - getPaddingRight() - 2 * mThumbView.getLeftRightMargin()); int target = (int) (mTickCount * percent + 0.5f); mThumbViewOffsetHelper.setLeftAndRightOffset((int) (target * step)); safeSetCurrentProgress(target); @@ -338,6 +351,18 @@ 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); @@ -442,10 +467,9 @@ public void setConstraintThumbInMoving(boolean constraintThumbInMoving) { mConstraintThumbInMoving = constraintThumbInMoving; } - private void calculateByThumbPosition() { + private void calculateByThumbPosition(int maxOffset) { View thumbView = convertThumbToView(); - float percent = mThumbViewOffsetHelper.getLeftAndRightOffset() * 1f / - (getWidth() - getPaddingLeft() - getPaddingRight() - thumbView.getWidth()); + float percent = mThumbViewOffsetHelper.getLeftAndRightOffset() * 1f / maxOffset; safeSetCurrentProgress(QMUILangHelper.constrain( (int) (mTickCount * percent + 0.5f), 0, @@ -518,6 +542,8 @@ public interface Callback { 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 { @@ -546,6 +572,11 @@ public void onStartMoving(QMUISlider slider, int progress, int tickCount) { public void onStopMoving(QMUISlider slider, int progress, int tickCount) { } + + @Override + public void onLongTouch(QMUISlider slider, int progress, int tickCount) { + + } } @@ -607,4 +638,19 @@ public SimpleArrayMap<String, Integer> 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); + } + } + } } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUITopBar.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUITopBar.java index 294ad3e6d..73a883a29 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUITopBar.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUITopBar.java @@ -22,7 +22,9 @@ 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; @@ -34,6 +36,10 @@ 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; @@ -47,15 +53,13 @@ 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; -import androidx.annotation.Nullable; -import androidx.collection.SimpleArrayMap; - -import org.jetbrains.annotations.NotNull; - /** * A standard toolbar for use within application content. * <p> @@ -73,13 +77,17 @@ public class QMUITopBar extends QMUIRelativeLayout implements IQMUISkinHandlerVi private View mCenterView; // 中间的 View private LinearLayout mTitleContainerView; // 包裹 title 和 subTitle 的容器 private QMUIQQFaceView mTitleView; // 显示 title 文字的 TextView - private QMUIQQFaceView mSubTitleView; // 显示 subTitle 文字的 TextView + private QMUISpanTouchFixTextView mSubTitleView; // 显示 subTitle 文字的 TextView private List<View> mLeftViewList; private List<View> 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; @@ -91,9 +99,11 @@ public class QMUITopBar extends QMUIRelativeLayout implements IQMUISkinHandlerVi 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<String, Integer> sDefaultSkinAttrs; @@ -131,9 +141,11 @@ void init(Context context, AttributeSet attrs) { 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, QMUIDisplayHelper.sp2px(context, 16)); + 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)); @@ -144,6 +156,25 @@ void init(Context context, AttributeSet attrs, int defStyleAttr) { 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(); } @@ -197,7 +228,7 @@ public QMUIQQFaceView setTitle(int resId) { * @param title TopBar 的标题 */ public QMUIQQFaceView setTitle(String title) { - QMUIQQFaceView titleView = getTitleView(); + QMUIQQFaceView titleView = ensureTitleView(); titleView.setText(title); if (QMUILangHelper.isNullOrEmpty(title)) { titleView.setVisibility(GONE); @@ -214,18 +245,24 @@ public CharSequence getTitle() { return mTitleView.getText(); } + @Nullable + public QMUIQQFaceView getTitleView(){ + return mTitleView; + } + public void showTitleView(boolean toShow) { if (mTitleView != null) { mTitleView.setVisibility(toShow ? VISIBLE : GONE); } } - private QMUIQQFaceView getTitleView() { + private QMUIQQFaceView ensureTitleView() { if (mTitleView == null) { mTitleView = new QMUIQQFaceView(getContext()); mTitleView.setGravity(Gravity.CENTER); mTitleView.setSingleLine(true); - mTitleView.setEllipsize(TruncateAt.MIDDLE); + 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); @@ -256,8 +293,8 @@ private void updateTitleViewStyle() { * * @param subTitle TopBar 的副标题 */ - public QMUIQQFaceView setSubTitle(String subTitle) { - QMUIQQFaceView subTitleView = getSubTitleView(); + public QMUISpanTouchFixTextView setSubTitle(CharSequence subTitle) { + QMUISpanTouchFixTextView subTitleView = ensureSubTitleView(); subTitleView.setText(subTitle); if (QMUILangHelper.isNullOrEmpty(subTitle)) { subTitleView.setVisibility(GONE); @@ -274,17 +311,18 @@ public QMUIQQFaceView setSubTitle(String subTitle) { * * @param resId TopBar 的副标题 resId */ - public QMUIQQFaceView setSubTitle(int resId) { + public QMUISpanTouchFixTextView setSubTitle(int resId) { return setSubTitle(getResources().getString(resId)); } - private QMUIQQFaceView getSubTitleView() { + private QMUISpanTouchFixTextView ensureSubTitleView() { if (mSubTitleView == null) { - mSubTitleView = new QMUIQQFaceView(getContext()); + mSubTitleView = new QMUISpanTouchFixTextView(getContext()); mSubTitleView.setGravity(Gravity.CENTER); mSubTitleView.setSingleLine(true); - mSubTitleView.setEllipsize(TruncateAt.MIDDLE); - mSubTitleView.setTextSize(mSubTitleTextSize); + 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); @@ -297,6 +335,11 @@ private QMUIQQFaceView getSubTitleView() { return mSubTitleView; } + @Nullable + public QMUISpanTouchFixTextView getSubTitleView(){ + return mSubTitleView; + } + /** * 设置 TopBar 的 gravity,用于控制 title 和 subtitle 的对齐方式 * @@ -452,12 +495,17 @@ public void addRightView(View view, int viewId, LayoutParams layoutParams) { addView(view, layoutParams); } + public LayoutParams generateTopBarImageButtonLayoutParams(){ + return generateTopBarImageButtonLayoutParams(-1, -1); + } + /** * 生成一个 LayoutParams,当把 Button addView 到 TopBar 时,使用这个 LayouyParams */ - public LayoutParams generateTopBarImageButtonLayoutParams() { - LayoutParams lp = new LayoutParams(mTopBarImageBtnWidth, mTopBarImageBtnHeight); - lp.topMargin = Math.max(0, (getTopBarHeight() - mTopBarImageBtnHeight) / 2); + 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; } @@ -466,6 +514,10 @@ 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 的右侧 * @@ -474,9 +526,9 @@ public QMUIAlphaImageButton addRightImageButton(int drawableResId, int viewId) { * @param followTintColor 换肤时使用 tintColor 更改它的颜色 * @return 返回生成的按钮 */ - public QMUIAlphaImageButton addRightImageButton(int drawableResId, boolean followTintColor, int viewId) { + public QMUIAlphaImageButton addRightImageButton(int drawableResId, boolean followTintColor, int viewId, int iconWidth, int iconHeight) { QMUIAlphaImageButton rightButton = generateTopBarImageButton(drawableResId, followTintColor); - this.addRightView(rightButton, viewId, generateTopBarImageButtonLayoutParams()); + this.addRightView(rightButton, viewId, generateTopBarImageButtonLayoutParams(iconWidth, iconHeight)); return rightButton; } @@ -484,6 +536,10 @@ 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 的左边 * @@ -492,9 +548,9 @@ public QMUIAlphaImageButton addLeftImageButton(int drawableResId, int viewId) { * @param followTintColor 换肤时使用 tintColor 更改它的颜色 * @return 返回生成的按钮 */ - public QMUIAlphaImageButton addLeftImageButton(int drawableResId, boolean followTintColor, int viewId) { + public QMUIAlphaImageButton addLeftImageButton(int drawableResId, boolean followTintColor, int viewId, int iconWidth, int iconHeight) { QMUIAlphaImageButton leftButton = generateTopBarImageButton(drawableResId, followTintColor); - this.addLeftView(leftButton, viewId, generateTopBarImageButtonLayoutParams()); + this.addLeftView(leftButton, viewId, generateTopBarImageButtonLayoutParams(iconWidth, iconHeight)); return leftButton; } @@ -579,6 +635,7 @@ private Button generateTopBarTextButton(String text) { 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); @@ -617,6 +674,12 @@ private QMUIAlphaImageButton generateTopBarImageButton(int imageResourceId, bool * @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); } @@ -787,4 +850,17 @@ public void handle(@NotNull QMUISkinManager manager, int skinIndex, @NotNull Res public SimpleArrayMap<String, Integer> 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); + } } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUITopBarLayout.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUITopBarLayout.java index 92eced363..6cb10a874 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUITopBarLayout.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUITopBarLayout.java @@ -24,22 +24,24 @@ 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 androidx.collection.SimpleArrayMap; +import com.qmuiteam.qmui.util.QMUIWindowInsetHelper; +import com.qmuiteam.qmui.widget.textview.QMUISpanTouchFixTextView; /** * 这是一个对 {@link QMUITopBar} 的代理类,需要它的原因是: * 我们用 fitSystemWindows 实现沉浸式状态栏后,需要将 {@link QMUITopBar} 的背景衍生到状态栏后面,这个时候 fitSystemWindows 是通过 * 更改 padding 实现的,而 {@link QMUITopBar} 是在高度固定的前提下做各种行为的,例如按钮的垂直居中,因此我们需要在外面包裹一层并消耗 padding - * <p> - * 这个类一般是配合 {@link QMUIWindowInsetLayout} 使用,并需要设置 fitSystemWindows 为 true - * </p> * * @author cginechen * @date 2016-11-26 @@ -63,10 +65,16 @@ public QMUITopBarLayout(Context context, AttributeSet attrs, int defStyleAttr) { 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() { @@ -89,14 +97,24 @@ public void showTitleView(boolean toShow) { mTopBar.showTitleView(toShow); } - public QMUIQQFaceView setSubTitle(int resId) { + public QMUISpanTouchFixTextView setSubTitle(int resId) { return mTopBar.setSubTitle(resId); } - public QMUIQQFaceView setSubTitle(String subTitle) { + 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); } @@ -121,10 +139,26 @@ 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); } @@ -189,4 +223,8 @@ public void setDefaultSkinAttr(String name, int attr) { public SimpleArrayMap<String, Integer> getDefaultSkinAttrs() { return mDefaultSkinAttrs; } + + public void eachLeftRightView(@NonNull QMUITopBar.Action action){ + mTopBar.eachLeftRightView(action); + } } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIViewPager.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIViewPager.java index 00b0f037d..543bd7e14 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIViewPager.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIViewPager.java @@ -18,32 +18,29 @@ import android.content.Context; import android.database.DataSetObserver; -import android.graphics.Rect; -import android.os.Build; import android.os.Parcelable; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; -import com.qmuiteam.qmui.util.QMUIWindowInsetHelper; - 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 implements IWindowInsetLayout { +public class QMUIViewPager extends ViewPager { private static final int DEFAULT_INFINITE_RATIO = 100; private boolean mIsSwipeable = true; private boolean mIsInMeasure = false; - private QMUIWindowInsetHelper mQMUIWindowInsetHelper; private boolean mEnableLoop = false; private int mInfiniteRatio = DEFAULT_INFINITE_RATIO; @@ -53,12 +50,9 @@ public QMUIViewPager(Context context) { public QMUIViewPager(Context context, AttributeSet attrs) { super(context, attrs); - mQMUIWindowInsetHelper = new QMUIWindowInsetHelper(this, this); + QMUIWindowInsetHelper.overrideWithDoNotHandleWindowInsets(this); } - - - public void setSwipeable(boolean enable) { mIsSwipeable = enable; } @@ -82,25 +76,30 @@ public void setEnableLoop(boolean enableLoop) { getAdapter().notifyDataSetChanged(); } } - } @Override - public void addView(View child, int index, ViewGroup.LayoutParams params) { - super.addView(child, index, params); - ViewCompat.requestApplyInsets(this); + public void onViewAdded(View child) { + super.onViewAdded(child); + ViewCompat.requestApplyInsets(child); } @Override public boolean onTouchEvent(MotionEvent ev) { - return mIsSwipeable && super.onTouchEvent(ev); - + try { + return mIsSwipeable && super.onTouchEvent(ev); + } catch (IllegalArgumentException ignore) { + return false; + } } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { - return mIsSwipeable && super.onInterceptTouchEvent(ev); - + try { + return mIsSwipeable && super.onInterceptTouchEvent(ev); + } catch (IllegalArgumentException ignore) { + return false; + } } @Override @@ -114,25 +113,6 @@ public boolean isInMeasure() { return mIsInMeasure; } - @SuppressWarnings("deprecation") - @Override - protected boolean fitSystemWindows(Rect insets) { - if (Build.VERSION.SDK_INT >= 19 && Build.VERSION.SDK_INT < 21) { - return applySystemWindowInsets19(insets); - } - return super.fitSystemWindows(insets); - } - - @Override - public boolean applySystemWindowInsets19(Rect insets) { - return mQMUIWindowInsetHelper.defaultApplySystemWindowInsets19(this, insets); - } - - @Override - public boolean applySystemWindowInsets21(Object insets) { - return mQMUIWindowInsetHelper.defaultApplySystemWindowInsets21(this, insets); - } - @Override public void setAdapter(PagerAdapter adapter) { if (adapter instanceof QMUIPagerAdapter) { diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIWindowInsetLayout.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIWindowInsetLayout.java index af5502bc7..a16ae061d 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIWindowInsetLayout.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIWindowInsetLayout.java @@ -17,28 +17,16 @@ package com.qmuiteam.qmui.widget; import android.content.Context; -import android.content.res.Configuration; -import android.graphics.Rect; -import android.os.Build; -import androidx.core.view.ViewCompat; 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; -/** - * From: https://github.com/oxoooo/earth/blob/30bd82fac7867be596bddf3bd0b32d8be3800665/app/src/main/java/ooo/oxo/apps/earth/widget/WindowInsetsFrameLayout.java - * 教程(英文): https://medium.com/google-developers/why-would-i-want-to-fitssystemwindows-4e26d9ce1eec#.6i7s7gyam - * 教程翻译: https://github.com/bboyfeiyu/android-tech-frontier/blob/master/issue-35/%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E4%BB%AC%E8%A6%81%E7%94%A8fitsSystemWindows.md - * <p> - * 对于Keyboard的处理我们需要格外小心,这个组件不能只是处理状态栏,因为android还存在NavBar - * 当windowInsets.bottom > 100dp的时候,我们认为是弹起了键盘。一旦弹起键盘,那么将由QMUIWindowInsetLayout消耗掉,其子view的windowInsets.bottom传递为0 - * - * @author cginechen - * @date 2016-03-25 - */ -public class QMUIWindowInsetLayout extends QMUIFrameLayout implements IWindowInsetLayout { - protected QMUIWindowInsetHelper mQMUIWindowInsetHelper; +@Deprecated +public class QMUIWindowInsetLayout extends QMUIFrameLayout { public QMUIWindowInsetLayout(Context context) { this(context, null); @@ -50,39 +38,11 @@ public QMUIWindowInsetLayout(Context context, AttributeSet attrs) { public QMUIWindowInsetLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - mQMUIWindowInsetHelper = new QMUIWindowInsetHelper(this, this); - } - - - @SuppressWarnings("deprecation") - @Override - protected boolean fitSystemWindows(Rect insets) { - if (Build.VERSION.SDK_INT >= 19 && Build.VERSION.SDK_INT < 21) { - return applySystemWindowInsets19(insets); - } - return super.fitSystemWindows(insets); - } - - @Override - public boolean applySystemWindowInsets19(Rect insets) { - return mQMUIWindowInsetHelper.defaultApplySystemWindowInsets19(this, insets); - } - - @Override - public boolean applySystemWindowInsets21(Object insets) { - return mQMUIWindowInsetHelper.defaultApplySystemWindowInsets21(this, insets); - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - ViewCompat.requestApplyInsets(this); } @Override - protected void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - // xiaomi 8 not reapply insets default... - ViewCompat.requestApplyInsets(this); + public void onViewAdded(View child) { + super.onViewAdded(child); + QMUIWindowInsetHelper.handleWindowInsets(child, WindowInsetsCompat.Type.statusBars() | WindowInsetsCompat.Type.displayCutout()); } } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIWindowInsetLayout2.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIWindowInsetLayout2.java index 80665c921..7abd1e71d 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIWindowInsetLayout2.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIWindowInsetLayout2.java @@ -17,31 +17,16 @@ package com.qmuiteam.qmui.widget; import android.content.Context; -import android.content.res.Configuration; -import android.graphics.Rect; -import android.os.Build; 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; -import androidx.constraintlayout.widget.ConstraintLayout; -import androidx.core.view.ViewCompat; - -/** - * From: https://github.com/oxoooo/earth/blob/30bd82fac7867be596bddf3bd0b32d8be3800665/app/src/main/java/ooo/oxo/apps/earth/widget/WindowInsetsFrameLayout.java - * 教程(英文): https://medium.com/google-developers/why-would-i-want-to-fitssystemwindows-4e26d9ce1eec#.6i7s7gyam - * 教程翻译: https://github.com/bboyfeiyu/android-tech-frontier/blob/master/issue-35/%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E4%BB%AC%E8%A6%81%E7%94%A8fitsSystemWindows.md - * <p> - * 对于Keyboard的处理我们需要格外小心,这个组件不能只是处理状态栏,因为android还存在NavBar - * 当windowInsets.bottom > 100dp的时候,我们认为是弹起了键盘。一旦弹起键盘,那么将由QMUIWindowInsetLayout消耗掉,其子view的windowInsets.bottom传递为0 - * - * @author cginechen - * @date 2016-03-25 - */ -public class QMUIWindowInsetLayout2 extends QMUIConstraintLayout implements IWindowInsetLayout { - protected QMUIWindowInsetHelper mQMUIWindowInsetHelper; - +@Deprecated +public class QMUIWindowInsetLayout2 extends QMUIConstraintLayout { public QMUIWindowInsetLayout2(Context context) { this(context, null); } @@ -52,39 +37,16 @@ public QMUIWindowInsetLayout2(Context context, AttributeSet attrs) { public QMUIWindowInsetLayout2(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - mQMUIWindowInsetHelper = new QMUIWindowInsetHelper(this, this); - } - - - @SuppressWarnings("deprecation") - @Override - protected boolean fitSystemWindows(Rect insets) { - if (Build.VERSION.SDK_INT >= 19 && Build.VERSION.SDK_INT < 21) { - return applySystemWindowInsets19(insets); - } - return super.fitSystemWindows(insets); - } - - @Override - public boolean applySystemWindowInsets19(Rect insets) { - return mQMUIWindowInsetHelper.defaultApplySystemWindowInsets19(this, insets); - } - - @Override - public boolean applySystemWindowInsets21(Object insets) { - return mQMUIWindowInsetHelper.defaultApplySystemWindowInsets21(this, insets); } @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - ViewCompat.requestApplyInsets(this); + public void setFitsSystemWindows(boolean fitSystemWindows) { + // do nothing. } @Override - protected void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - // xiaomi 8 not reapply insets default... - ViewCompat.requestApplyInsets(this); + public void onViewAdded(View view) { + super.onViewAdded(view); + QMUIWindowInsetHelper.handleWindowInsets(view, WindowInsetsCompat.Type.statusBars() | WindowInsetsCompat.Type.displayCutout()); } } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBaseDialog.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBaseDialog.java index 604506930..2bf004731 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBaseDialog.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBaseDialog.java @@ -16,19 +16,21 @@ 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 com.qmuiteam.qmui.skin.QMUISkinLayoutInflaterFactory; -import com.qmuiteam.qmui.skin.QMUISkinManager; - 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; @@ -112,4 +114,24 @@ protected boolean shouldWindowCloseOnTouchOutside() { } 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){ + } + } + } } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBottomSheet.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBottomSheet.java index 3a65f118b..c34837c86 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBottomSheet.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBottomSheet.java @@ -16,14 +16,15 @@ 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.Build; import android.os.Bundle; -import android.text.Layout; import android.util.Pair; +import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; @@ -32,26 +33,24 @@ import android.view.WindowManager; import android.widget.LinearLayout; -import com.google.android.material.bottomsheet.BottomSheetBehavior; -import com.qmuiteam.qmui.R; -import com.qmuiteam.qmui.layout.QMUIPriorityLinearLayout; -import com.qmuiteam.qmui.skin.QMUISkinLayoutInflaterFactory; - -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.coordinatorlayout.widget.CoordinatorLayout; -import androidx.core.view.LayoutInflaterCompat; import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import static com.qmuiteam.qmui.layout.IQMUILayout.HIDE_RADIUS_SIDE_BOTTOM; +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} 在界面底部升起和降下。 @@ -143,15 +142,31 @@ protected void onSetCancelable(boolean 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) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); - window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); - } + 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); @@ -210,17 +225,21 @@ public void show() { mOnBottomSheetShowListener.onShow(); } if (mBehavior.getState() != BottomSheetBehavior.STATE_EXPANDED) { - mRootView.postOnAnimation(new Runnable() { - @Override - public void run() { - mBehavior.setState(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(); } @@ -407,6 +426,7 @@ public BottomListSheetBuilder addContentFooterView(@NonNull View view) { return this; } + @Nullable @Override protected View onCreateContentView(@NonNull final QMUIBottomSheet bottomSheet, @@ -498,6 +518,8 @@ public QMUIBottomSheetGridItemView create(@NonNull QMUIBottomSheet bottomSheet, private ArrayList<QMUIBottomSheetGridItemModel> 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); @@ -505,6 +527,11 @@ public BottomGridSheetBuilder(Context context) { 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: @@ -548,6 +575,11 @@ public BottomGridSheetBuilder setOnSheetItemClickListener(OnSheetItemClickListen return this; } + public BottomGridSheetBuilder setItemWidthCalculator(QMUIBottomSheetGridLineLayout.ItemWidthCalculator itemWidthCalculator) { + mItemWidthCalculator = itemWidthCalculator; + return this; + } + @Override public void onClick(View v) { if (mOnSheetItemClickListener != null) { @@ -587,7 +619,7 @@ protected View onCreateContentView(@NonNull QMUIBottomSheet bottomSheet, new LinearLayout.LayoutParams(wrapContent, wrapContent))); } } - return new QMUIBottomSheetGridLineLayout(mDialog, firstLines, secondLines); + return new QMUIBottomSheetGridLineLayout(mDialog, mItemWidthCalculator, mLineGravity, firstLines, secondLines); } public interface OnSheetItemClickListener { diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBottomSheetBaseBuilder.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBottomSheetBaseBuilder.java index c8143d42c..dfc1abc78 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBottomSheetBaseBuilder.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBottomSheetBaseBuilder.java @@ -20,8 +20,9 @@ import android.content.DialogInterface; import android.view.View; import android.view.ViewGroup; -import android.widget.LinearLayout; -import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.layout.QMUIButton; @@ -32,9 +33,6 @@ import com.qmuiteam.qmui.util.QMUIResHelper; import com.qmuiteam.qmui.widget.textview.QMUISpanTouchFixTextView; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - public abstract class QMUIBottomSheetBaseBuilder<T extends QMUIBottomSheetBaseBuilder> { private Context mContext; protected QMUIBottomSheet mDialog; @@ -46,6 +44,7 @@ public abstract class QMUIBottomSheetBaseBuilder<T extends QMUIBottomSheetBaseBu private boolean mAllowDrag = false; private QMUISkinManager mSkinManager; private QMUIBottomSheetBehavior.DownDragDecisionMaker mDownDragDecisionMaker = null; + private boolean fitNav = true; public QMUIBottomSheetBaseBuilder(Context context) { mContext = context; @@ -73,6 +72,12 @@ public T setSkinManager(@Nullable QMUISkinManager 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; @@ -140,6 +145,8 @@ public QMUIBottomSheet build(int style) { mDialog.setRadius(mRadius); } mDialog.setSkinManager(mSkinManager); + mDialog.setFitNav(fitNav); + QMUIBottomSheetBehavior behavior = mDialog.getBehavior(); behavior.setAllowDrag(mAllowDrag); behavior.setDownDragDecisionMaker(mDownDragDecisionMaker); diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBottomSheetGridLineLayout.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBottomSheetGridLineLayout.java index eaf8859b3..07795a32c 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBottomSheetGridLineLayout.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBottomSheetGridLineLayout.java @@ -24,6 +24,8 @@ import android.widget.HorizontalScrollView; import android.widget.LinearLayout; +import androidx.annotation.Nullable; + import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.util.QMUIResHelper; @@ -32,19 +34,50 @@ 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<Pair<View, LinearLayout.LayoutParams>> mFirstLineViews; private List<Pair<View, LinearLayout.LayoutParams>> 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<Pair<View, LinearLayout.LayoutParams>> firstLineViews, List<Pair<View, LinearLayout.LayoutParams>> 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( @@ -112,7 +145,7 @@ protected HorizontalScrollView createHorScroller( LinearLayout linear = new LinearLayout(context); linear.setOrientation(LinearLayout.HORIZONTAL); - linear.setGravity(Gravity.CENTER_VERTICAL); + linear.setGravity(mLineGravity); linear.setPadding(linePaddingHor, 0, linePaddingHor, 0); scroller.addView(linear, new HorizontalScrollView.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); @@ -131,27 +164,14 @@ protected void measureChild(View child, int parentWidthMeasureSpec, int parentHe 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); + } - final int parentSpacing = width - paddingLeft - paddingRight; - int itemWidth = miniItemWidth; - // there is no more space for the last one item. then stretch the item width - if (calculateCount >= 3 - && parentSpacing - calculateCount * itemWidth > 0 - && parentSpacing - calculateCount * 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 * calculateCount > parentSpacing) { - int count = (width - paddingLeft) / itemWidth; - itemWidth = (int) ((width - paddingLeft) / (count + .5f)); - } - return itemWidth; + public interface ItemWidthCalculator { + int calculate(Context context, int width, int miniWidth, int itemCount, int paddingLeft, int paddingRight); } } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBottomSheetListAdapter.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBottomSheetListAdapter.java index 70d3150c5..d8f8cc6b2 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBottomSheetListAdapter.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBottomSheetListAdapter.java @@ -19,13 +19,13 @@ import android.view.View; import android.view.ViewGroup; -import java.util.ArrayList; -import java.util.List; - 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<QMUIBottomSheetListAdapter.VH> { public static final int ITEM_TYPE_HEADER = 1; @@ -124,14 +124,14 @@ public int getItemCount() { return mData.size() + (mHeaderView != null ? 1 : 0) + (mFooterView != null ? 1 : 0); } - static class VH extends RecyclerView.ViewHolder { + public static class VH extends RecyclerView.ViewHolder { public VH(@NonNull View itemView) { super(itemView); } } - interface OnItemClickListener { + public interface OnItemClickListener { void onClick(VH vh, int dataPos, QMUIBottomSheetListItemModel model); } } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBottomSheetListItemDecoration.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBottomSheetListItemDecoration.java index 83eb62178..14d66a446 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBottomSheetListItemDecoration.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBottomSheetListItemDecoration.java @@ -22,14 +22,14 @@ 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 androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - import org.jetbrains.annotations.NotNull; public class QMUIBottomSheetListItemDecoration extends RecyclerView.ItemDecoration @@ -40,7 +40,8 @@ public class QMUIBottomSheetListItemDecoration extends RecyclerView.ItemDecorati public QMUIBottomSheetListItemDecoration(Context context) { mSeparatorPaint = new Paint(); - mSeparatorPaint.setStrokeWidth(1); + 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) { diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBottomSheetRootLayout.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBottomSheetRootLayout.java index 2f4918cc2..6ea7a27c8 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBottomSheetRootLayout.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBottomSheetRootLayout.java @@ -20,7 +20,6 @@ import android.util.AttributeSet; import com.qmuiteam.qmui.R; -import com.qmuiteam.qmui.layout.QMUILinearLayout; import com.qmuiteam.qmui.layout.QMUIPriorityLinearLayout; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; @@ -58,7 +57,7 @@ public QMUIBottomSheetRootLayout(Context context, AttributeSet attrs) { protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSize = MeasureSpec.getSize(widthMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); - if(widthSize > mMaxWidth){ + if (widthSize > mMaxWidth) { widthMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxWidth, widthMode); } int heightSize = MeasureSpec.getSize(heightMeasureSpec); diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIDialogRootLayout.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIDialogRootLayout.java index 094d749bc..b06d1672b 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIDialogRootLayout.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIDialogRootLayout.java @@ -59,6 +59,7 @@ public QMUIDialogRootLayout(@NonNull Context context, @NonNull QMUIDialogView di 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) { diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIDialogView.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIDialogView.java index 83915e3e2..d7edc8f71 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIDialogView.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIDialogView.java @@ -20,10 +20,11 @@ import android.graphics.Canvas; import android.util.AttributeSet; -import com.qmuiteam.qmui.layout.QMUIConstraintLayout; - import androidx.annotation.Nullable; +import com.qmuiteam.qmui.R; +import com.qmuiteam.qmui.layout.QMUIConstraintLayout; + /** * Created by cgspine on 2018/2/28. */ @@ -43,6 +44,7 @@ public QMUIDialogView(Context context, @Nullable AttributeSet attrs) { public QMUIDialogView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); + setId(R.id.qmui_dialog_layout); } public void setOnDecorationListener(OnDecorationListener onDecorationListener) { diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUITipDialog.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUITipDialog.java index b4dbc4851..358f909a8 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUITipDialog.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUITipDialog.java @@ -19,7 +19,6 @@ import android.app.Dialog; import android.content.Context; import android.graphics.drawable.Drawable; -import android.os.Bundle; import android.text.TextUtils; import android.util.TypedValue; import android.view.Gravity; @@ -29,6 +28,11 @@ 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; @@ -40,11 +44,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import androidx.annotation.IntDef; -import androidx.annotation.LayoutRes; -import androidx.annotation.Nullable; -import androidx.appcompat.widget.AppCompatImageView; - /** * 提供一个浮层展示在屏幕中间, 一般使用 {@link QMUITipDialog.Builder} 或 {@link QMUITipDialog.CustomBuilder} 生成。 * <ul> @@ -200,6 +199,7 @@ public QMUITipDialog create(boolean cancelable, int style) { 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)); diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/grouplist/QMUICommonListItemView.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/grouplist/QMUICommonListItemView.java index 24bf86969..813d19375 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/grouplist/QMUICommonListItemView.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/grouplist/QMUICommonListItemView.java @@ -28,6 +28,11 @@ 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; @@ -38,12 +43,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import androidx.annotation.IntDef; -import androidx.appcompat.widget.AppCompatCheckBox; -import androidx.appcompat.widget.AppCompatImageView; -import androidx.constraintlayout.widget.ConstraintLayout; -import androidx.constraintlayout.widget.Placeholder; - /** * 作为通用列表 {@link QMUIGroupListView} 里的 item 使用,也可以单独使用。 * 支持以下样式: @@ -139,8 +138,6 @@ public class QMUICommonListItemView extends QMUIConstraintLayout { protected CheckBox mSwitch; private ImageView mRedDot; private ImageView mNewTipView; - private Placeholder mAfterTitleHolder; - private Placeholder mBeforeAccessoryHolder; private boolean mDisableSwitchSelf = false; private int mTipShown = TIP_SHOW_NOTHING; @@ -173,11 +170,6 @@ protected void init(Context context, AttributeSet attrs, int defStyleAttr) { 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); - mAfterTitleHolder = findViewById(R.id.group_list_item_holder_after_title); - mBeforeAccessoryHolder = findViewById(R.id.group_list_item_holder_before_accessory); - - mAfterTitleHolder.setEmptyVisibility(View.GONE); - mBeforeAccessoryHolder.setEmptyVisibility(View.GONE); mTextView.setTextColor(initTitleColor); mDetailTextView.setTextColor(initDetailColor); mAccessoryView = findViewById(R.id.group_list_item_accessoryView); @@ -203,27 +195,10 @@ public void setImageDrawable(Drawable drawable) { } public void setTipPosition(@QMUICommonListItemTipPosition int tipPosition) { - mTipPosition = tipPosition; - if (mRedDot.getVisibility() == View.VISIBLE) { - if (mTipPosition == TIP_POSITION_LEFT) { - mAfterTitleHolder.setContentId(mRedDot.getId()); - mBeforeAccessoryHolder.setContentId(View.NO_ID); - } else { - mBeforeAccessoryHolder.setContentId(mRedDot.getId()); - mAfterTitleHolder.setContentId(View.NO_ID); - } - mNewTipView.setVisibility(View.GONE); - } else if (mNewTipView.getVisibility() == View.VISIBLE) { - if (mTipPosition == TIP_POSITION_LEFT) { - mAfterTitleHolder.setContentId(mNewTipView.getId()); - mBeforeAccessoryHolder.setContentId(View.NO_ID); - } else { - mBeforeAccessoryHolder.setContentId(mNewTipView.getId()); - mAfterTitleHolder.setContentId(View.NO_ID); - } - mRedDot.setVisibility(View.GONE); + if(mTipPosition != tipPosition){ + mTipPosition = tipPosition; + updateLayoutParams(); } - checkDetailLeftMargin(); } public CharSequence getText() { @@ -245,12 +220,15 @@ public void setText(CharSequence text) { * @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; } - updateTipShown(); + if(oldTipShown != mTipShown){ + updateLayoutParams(); + } } /** @@ -259,54 +237,17 @@ public void showRedDot(boolean isShow) { * @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; } - updateTipShown(); - } - - private void updateTipShown(){ - if(mTipShown == TIP_SHOW_RED_POINT){ - if (mTipPosition == TIP_POSITION_LEFT) { - mAfterTitleHolder.setContentId(mRedDot.getId()); - mBeforeAccessoryHolder.setContentId(View.NO_ID); - } else { - mBeforeAccessoryHolder.setContentId(mRedDot.getId()); - mAfterTitleHolder.setContentId(View.NO_ID); - } - }else if(mTipShown == TIP_SHOW_NEW){ - if (mTipPosition == TIP_POSITION_LEFT) { - mAfterTitleHolder.setContentId(mNewTipView.getId()); - mBeforeAccessoryHolder.setContentId(View.NO_ID); - } else { - mBeforeAccessoryHolder.setContentId(mNewTipView.getId()); - mAfterTitleHolder.setContentId(View.NO_ID); - } - }else{ - mAfterTitleHolder.setContentId(View.NO_ID); - mBeforeAccessoryHolder.setContentId(View.NO_ID); + if(oldTipShown != mTipShown){ + updateLayoutParams(); } - mNewTipView.setVisibility(mTipShown == TIP_SHOW_NEW ? View.VISIBLE : View.GONE); - mRedDot.setVisibility(mTipShown == TIP_SHOW_RED_POINT ? View.VISIBLE : View.GONE); - checkDetailLeftMargin(); } - private void checkDetailLeftMargin() { - LayoutParams detailLp = (LayoutParams) mDetailTextView.getLayoutParams(); - if (mOrientation == VERTICAL) { - detailLp.leftMargin = 0; - } else { - if (mNewTipView.getVisibility() == View.GONE || mTipPosition == TIP_POSITION_LEFT) { - detailLp.leftMargin = QMUIResHelper.getAttrDimen( - getContext(), R.attr.qmui_common_list_item_detail_h_margin_with_title); - } else { - detailLp.leftMargin = QMUIResHelper.getAttrDimen( - getContext(), R.attr.qmui_common_list_item_detail_h_margin_with_title_large); - } - } - } public CharSequence getDetailText() { return mDetailTextView.getText(); @@ -331,49 +272,189 @@ public void setOrientation(@QMUICommonListItemOrientation int 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(); - if (orientation == VERTICAL) { + 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.horizontalChainStyle = LayoutParams.UNSET; titleLp.verticalChainStyle = LayoutParams.CHAIN_PACKED; titleLp.bottomToBottom = LayoutParams.UNSET; titleLp.bottomToTop = mDetailTextView.getId(); detailLp.horizontalChainStyle = LayoutParams.UNSET; detailLp.verticalChainStyle = LayoutParams.CHAIN_PACKED; - detailLp.leftToRight = LayoutParams.UNSET; 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.horizontalChainStyle = LayoutParams.CHAIN_SPREAD_INSIDE; titleLp.verticalChainStyle = LayoutParams.UNSET; titleLp.bottomToBottom = LayoutParams.PARENT_ID; titleLp.bottomToTop = LayoutParams.UNSET; - detailLp.horizontalChainStyle = LayoutParams.CHAIN_SPREAD_INSIDE; detailLp.verticalChainStyle = LayoutParams.UNSET; - detailLp.leftToRight = mTextView.getId(); detailLp.leftToLeft = LayoutParams.UNSET; - detailLp.horizontalBias = 0f; detailLp.topToTop = LayoutParams.PARENT_ID; detailLp.topToBottom = LayoutParams.UNSET; detailLp.topMargin = 0; - checkDetailLeftMargin(); + 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() { diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/popup/QMUIBasePopup.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/popup/QMUIBasePopup.java index 362579fb1..510e7498d 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/popup/QMUIBasePopup.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/popup/QMUIBasePopup.java @@ -27,16 +27,15 @@ 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; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.app.NavUtils; -import androidx.core.view.ViewCompat; - public abstract class QMUIBasePopup<T extends QMUIBasePopup> { public static final float DIM_AMOUNT_NOT_EXIST = -1f; public static final int NOT_SET = -1; @@ -48,7 +47,6 @@ public abstract class QMUIBasePopup<T extends QMUIBasePopup> { private float mDimAmount = DIM_AMOUNT_NOT_EXIST; private int mDimAmountAttr = 0; private PopupWindow.OnDismissListener mDismissListener; - private boolean mDismissIfOutsideTouch = true; private QMUISkinManager mSkinManager; private QMUISkinManager.OnSkinChangeListener mOnSkinChangeListener = new QMUISkinManager.OnSkinChangeListener() { @Override @@ -85,27 +83,30 @@ public boolean onTouch(View v, MotionEvent event) { }; + public QMUIBasePopup(Context context) { mContext = context; mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); mWindow = new PopupWindow(context); - initWindow(); - } - - private void initWindow() { 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(mDismissIfOutsideTouch); + dismissIfOutsideTouch(true); } protected void onSkinChange(int oldSkin, int newSkin){ @@ -131,8 +132,17 @@ public T skinManager(@Nullable QMUISkinManager 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) { - mDismissIfOutsideTouch = dismissIfOutsideTouch; mWindow.setOutsideTouchable(dismissIfOutsideTouch); if (dismissIfOutsideTouch) { mWindow.setTouchInterceptor(mOutsideTouchDismissListener); @@ -221,12 +231,6 @@ protected void onDismiss() { } public final void dismiss() { - removeOldAttachStateChangeListener(); - mAttachedViewRf = null; - if(mSkinManager != null){ - mSkinManager.unRegister(mWindow); - mSkinManager.removeSkinChangeListener(mOnSkinChangeListener); - } mWindow.dismiss(); } } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/popup/QMUIFullScreenPopup.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/popup/QMUIFullScreenPopup.java index f34124888..0bd288765 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/popup/QMUIFullScreenPopup.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/popup/QMUIFullScreenPopup.java @@ -16,60 +16,32 @@ package com.qmuiteam.qmui.widget.popup; -import android.animation.ValueAnimator; -import android.annotation.TargetApi; import android.content.Context; -import android.graphics.Rect; import android.graphics.drawable.Drawable; -import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.ImageView; -import com.qmuiteam.qmui.QMUIInterpolatorStaticHolder; +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.util.QMUIViewOffsetHelper; import com.qmuiteam.qmui.widget.IBlankTouchDetector; -import com.qmuiteam.qmui.widget.IWindowInsetKeyboardConsumer; -import com.qmuiteam.qmui.widget.QMUIWindowInsetLayout2; import java.util.ArrayList; -import androidx.annotation.Nullable; -import androidx.constraintlayout.widget.ConstraintLayout; -import androidx.core.view.GestureDetectorCompat; - 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<QMUIFullScreenPopup> { - - private static OnKeyBoardListener sOffsetKeyboardHeightListener; - private static OnKeyBoardListener sOffsetHalfKeyboardHeightListener; - - public static OnKeyBoardListener getOffsetKeyboardHeightListener() { - if (sOffsetKeyboardHeightListener == null) { - sOffsetKeyboardHeightListener = new KeyboardPercentOffsetListener(1f); - } - return sOffsetKeyboardHeightListener; - } - - public static OnKeyBoardListener getOffsetHalfKeyboardHeightListener() { - if (sOffsetHalfKeyboardHeightListener == null) { - sOffsetHalfKeyboardHeightListener = new KeyboardPercentOffsetListener(0.5f); - } - return sOffsetHalfKeyboardHeightListener; - } - - private OnBlankClickListener mOnBlankClickListener; private boolean mAddCloseBtn = false; private int mCloseIconAttr = R.attr.qmui_skin_support_popup_close_icon; @@ -82,6 +54,7 @@ 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); } @@ -119,22 +92,15 @@ public QMUIFullScreenPopup animStyle(int animStyle) { return this; } - public QMUIFullScreenPopup addView(View view, ConstraintLayout.LayoutParams lp, OnKeyBoardListener onKeyBoardListener) { - mViews.add(new ViewInfo(view, lp, onKeyBoardListener)); - return this; - } - public QMUIFullScreenPopup addView(View view, ConstraintLayout.LayoutParams lp) { - return addView(view, lp, null); - } - - public QMUIFullScreenPopup addView(View view, OnKeyBoardListener onKeyBoardListener) { - mViews.add(new ViewInfo(view, defaultContentLp(), onKeyBoardListener)); + mViews.add(new ViewInfo(view, lp)); return this; } + public QMUIFullScreenPopup addView(View view) { - return addView(view, defaultContentLp()); + mViews.add(new ViewInfo(view, defaultContentLp())); + return this; } private ConstraintLayout.LayoutParams defaultContentLp() { @@ -170,9 +136,9 @@ public void onClick(View v) { }); closeBtn.setFitsSystemWindows(true); Drawable drawable = null; - if(mCloseIcon != null){ + if (mCloseIcon != null) { drawable = mCloseIcon; - }else if(mCloseIconAttr != 0){ + } else if (mCloseIconAttr != 0) { QMUISkinValueBuilder builder = QMUISkinValueBuilder.acquire().src(mCloseIconAttr); QMUISkinHelper.setSkinValue(closeBtn, builder); builder.release(); @@ -182,7 +148,14 @@ public void onClick(View v) { 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"); } @@ -221,40 +194,46 @@ public interface OnBlankClickListener { void onBlankClick(QMUIFullScreenPopup popup); } - class RootView extends QMUIWindowInsetLayout2 implements IWindowInsetKeyboardConsumer { - private GestureDetectorCompat mGestureDetector; - private int mLastKeyboardShowHeight = 0; + class RootView extends QMUIConstraintLayout { + private boolean mShouldInvokeBlackClickWhenTouchUp = false; public RootView(Context context) { super(context); - mGestureDetector = new GestureDetectorCompat(context, new GestureDetector.SimpleOnGestureListener() { - @Override - public boolean onSingleTapUp(MotionEvent e) { - return true; - } - }); } @Override public boolean onTouchEvent(MotionEvent event) { - if (mGestureDetector.onTouchEvent(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(); - } - if (isBlank && mOnBlankClickListener != null) { + 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(); @@ -272,99 +251,23 @@ private View findChildViewUnder(float x, float y) { return null; } - @Override - public boolean applySystemWindowInsets19(Rect insets) { - super.applySystemWindowInsets19(insets); - return true; - } - - @Override - @TargetApi(21) - public boolean applySystemWindowInsets21(Object insets) { - super.applySystemWindowInsets21(insets); - return true; - } - - @Override - public void onHandleKeyboard(int keyboardInset) { - if (keyboardInset > 0) { - mLastKeyboardShowHeight = keyboardInset; - for (ViewInfo viewInfo : mViews) { - if (viewInfo.onKeyBoardListener != null) { - viewInfo.onKeyBoardListener.onKeyboardToggle(viewInfo.view, true, keyboardInset, getHeight()); - } - } - } else { - for (ViewInfo viewInfo : mViews) { - if (viewInfo.onKeyBoardListener != null) { - viewInfo.onKeyBoardListener.onKeyboardToggle(viewInfo.view, false, mLastKeyboardShowHeight, getHeight()); - } - } - } - } - @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; - QMUIViewOffsetHelper offsetHelper = (QMUIViewOffsetHelper) view.getTag(R.id.qmui_view_offset_helper); - if (offsetHelper != null) { - offsetHelper.onViewLayout(); - } + QMUIViewHelper.getOrCreateOffsetHelper(view).onViewLayout(); } } } class ViewInfo { - private OnKeyBoardListener onKeyBoardListener; private View view; private ConstraintLayout.LayoutParams lp; - public ViewInfo(View view, ConstraintLayout.LayoutParams lp, @Nullable OnKeyBoardListener onKeyBoardListener) { + public ViewInfo(View view, ConstraintLayout.LayoutParams lp) { this.view = view; this.lp = lp; - this.onKeyBoardListener = onKeyBoardListener; - } - } - - public static QMUIViewOffsetHelper getOrCreateViewOffsetHelper(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; - } - - public interface OnKeyBoardListener { - void onKeyboardToggle(View view, boolean toShow, int keyboardHeight, int rootViewHeight); - } - - public static class KeyboardPercentOffsetListener implements OnKeyBoardListener { - private float mPercent; - private ValueAnimator mAnimator; - - public KeyboardPercentOffsetListener(float percent) { - mPercent = percent; - } - - @Override - public void onKeyboardToggle(View view, boolean toShow, int keyboardHeight, int rootViewHeight) { - final QMUIViewOffsetHelper offsetHelper = QMUIFullScreenPopup.getOrCreateViewOffsetHelper(view); - if (mAnimator != null) { - QMUIViewHelper.clearValueAnimator(mAnimator); - } - int target = toShow ? (int) (-keyboardHeight * mPercent) : 0; - mAnimator = ValueAnimator.ofInt(offsetHelper.getTopAndBottomOffset(), target); - mAnimator.setInterpolator(QMUIInterpolatorStaticHolder.FAST_OUT_SLOW_IN_INTERPOLATOR); - mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - offsetHelper.setTopAndBottomOffset((Integer) animation.getAnimatedValue()); - } - }); - mAnimator.start(); } } } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/popup/QMUINormalPopup.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/popup/QMUINormalPopup.java index 450b70add..6b512d225 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/popup/QMUINormalPopup.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/popup/QMUINormalPopup.java @@ -22,12 +22,22 @@ 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; @@ -36,16 +46,11 @@ 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; -import androidx.annotation.AnimRes; -import androidx.annotation.IntDef; -import androidx.annotation.LayoutRes; -import androidx.annotation.NonNull; - -import org.jetbrains.annotations.NotNull; - public class QMUINormalPopup<T extends QMUIBasePopup> extends QMUIBasePopup<T> { public static final int ANIM_AUTO = 0; @@ -55,7 +60,7 @@ public class QMUINormalPopup<T extends QMUIBasePopup> extends QMUIBasePopup<T> { 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}) - @interface AnimStyle { + public @interface AnimStyle { } public static final int DIRECTION_TOP = 0; @@ -76,14 +81,16 @@ public class QMUINormalPopup<T extends QMUIBasePopup> extends QMUIBasePopup<T> { private boolean mShowArrow = true; private boolean mAddShadow = false; private int mRadius = NOT_SET; - private int mBorderColor = 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 = 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; @@ -95,12 +102,21 @@ public class QMUINormalPopup<T extends QMUIBasePopup> extends QMUIBasePopup<T> { 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) { + 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) { @@ -196,6 +212,29 @@ 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; @@ -203,6 +242,7 @@ public T borderWidth(int borderWidth) { public T borderColor(int borderColor) { mBorderColor = borderColor; + mIsBorderColorSet = true; return (T) this; } @@ -224,28 +264,35 @@ public int getBorderColorAttr() { 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 int[] anchorLocation = new int[2]; + private Rect anchorFrame = new Rect(); Rect visibleWindowFrame = new Rect(); int width; int height; int x; int y; - View anchor; + int anchorHeight; int anchorCenter; int direction = mPreferredDirection; int contentWidthMeasureSpec; @@ -255,13 +302,22 @@ class ShowInfo { int decorationTop = 0; int decorationBottom = 0; - ShowInfo(View anchor) { - this.anchor = anchor; + 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); - anchorCenter = anchorLocation[0] + anchor.getWidth() / 2; + 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()); } @@ -299,14 +355,19 @@ private boolean shouldShowShadow() { } 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"); } - ShowInfo showInfo = new ShowInfo(anchor); + decorateContentView(); + ShowInfo showInfo = new ShowInfo(anchor, anchorAreaLeft, anchorAreaTop, anchorAreaRight, anchorAreaBottom); calculateWindowSize(showInfo); calculateXY(showInfo); adjustShowInfo(showInfo); - decorateContentView(showInfo); + mDecorRootView.setShowInfo(showInfo); setAnimationStyle(showInfo.anchorProportion(), showInfo.direction); mWindow.setWidth(showInfo.windowWidth()); mWindow.setHeight(showInfo.windowHeight()); @@ -314,17 +375,16 @@ public T show(@NonNull View anchor) { return (T) this; } - - private void decorateContentView(ShowInfo showInfo) { + private void decorateContentView() { ContentView contentView = ContentView.wrap(mContentView, mInitWidth, mInitHeight); QMUISkinValueBuilder builder = QMUISkinValueBuilder.acquire(); - if (mBorderColor != NOT_SET) { + if (mIsBorderColorSet) { mBorderUsedColor = mBorderColor; } else if (mBorderColorAttr != 0) { mBorderUsedColor = QMUIResHelper.getAttrColor(mContext, mBorderColorAttr); builder.border(mBorderColorAttr); } - if (mBgColor != NOT_SET) { + if (mIsBgColorSet) { mBgUsedColor = mBgColor; } else if (mBgColorAttr != 0) { mBgUsedColor = QMUIResHelper.getAttrColor(mContext, mBgColorAttr); @@ -350,10 +410,7 @@ private void decorateContentView(ShowInfo showInfo) { } else { contentView.setRadius(mRadius); } - - DecorRootView decorRootView = new DecorRootView(mContext, showInfo); - decorRootView.setContentView(contentView); - mWindow.setContentView(decorRootView); + mDecorRootView.setContentView(contentView); } private void adjustShowInfo(ShowInfo showInfo) { @@ -402,7 +459,7 @@ private void adjustShowInfo(ShowInfo showInfo) { } if (showInfo.direction == DIRECTION_BOTTOM) { if (shouldShowShadow()) { - showInfo.y += mArrowHeight; + showInfo.y += Math.min(mShadowInset, mArrowHeight); } showInfo.decorationTop = Math.max(showInfo.decorationTop, mArrowHeight); } else if (showInfo.direction == DIRECTION_TOP) { @@ -435,14 +492,14 @@ private void handleDirection(ShowInfo showInfo, int currentDirection, int nextDi showInfo.y = showInfo.visibleWindowFrame.top + (showInfo.getVisibleHeight() - showInfo.height) / 2; showInfo.direction = DIRECTION_CENTER_IN_SCREEN; } else if (currentDirection == DIRECTION_TOP) { - showInfo.y = showInfo.anchorLocation[1] - showInfo.height - mOffsetYIfTop; + 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.anchorLocation[1] + showInfo.anchor.getHeight() + mOffsetYIfBottom; + 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 { @@ -494,7 +551,7 @@ private void calculateWindowSize(ShowInfo showInfo) { } } - if (needMeasureForWidth || needMeasureForHeight) { + if (mForceMeasureIfNeeded && (needMeasureForWidth || needMeasureForHeight)) { mContentView.measure( showInfo.contentWidthMeasureSpec, showInfo.contentHeightMeasureSpec); if (needMeasureForWidth) { @@ -555,6 +612,8 @@ class DecorRootView extends FrameLayout implements IQMUISkinDispatchInterceptor 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; @@ -569,14 +628,17 @@ public void run() { } }; - private DecorRootView(Context context, ShowInfo showInfo) { + private DecorRootView(Context context) { super(context); - mShowInfo = showInfo; 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) { @@ -592,6 +654,10 @@ public void setContentView(View 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(); @@ -607,7 +673,7 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - if (mContentView != null) { + if (mContentView != null && mShowInfo != null) { mContentView.layout(mShowInfo.decorationLeft, mShowInfo.decorationTop, mShowInfo.width + mShowInfo.decorationLeft, mShowInfo.height + mShowInfo.decorationTop); @@ -622,10 +688,10 @@ protected void onAttachedToWindow() { @Override public boolean intercept(int skinIndex, @NotNull Resources.Theme theme) { - if (mBorderColor == NOT_SET && mBorderColorAttr != 0) { + if (!mIsBorderColorSet && mBorderColorAttr != 0) { mBorderUsedColor = QMUIResHelper.getAttrColor(theme, mBorderColorAttr); } - if (mBgColor == NOT_SET && mBgColorAttr != 0) { + if (!mIsBgColorSet && mBgColorAttr != 0) { mBgUsedColor = QMUIResHelper.getAttrColor(theme, mBgColorAttr); } return false; @@ -634,51 +700,67 @@ public boolean intercept(int skinIndex, @NotNull Resources.Theme theme) { @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 - 1; + int t = mShowInfo.decorationTop + mShowInfo.height - mBorderWidth; canvas.translate(l, t); mArrowPath.reset(); - mArrowPath.setLastPoint(0, 0); - mArrowPath.lineTo(mArrowWidth / 2, mArrowHeight); - mArrowPath.lineTo(mArrowWidth, 0); + 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.drawLine(0, 0, mArrowWidth / 2, mArrowHeight, mArrowPaint); - canvas.drawLine(mArrowWidth / 2, mArrowHeight, mArrowWidth, 0, mArrowPaint); + 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 + 1; + int t = mShowInfo.decorationTop + mBorderWidth; canvas.translate(l, t); mArrowPath.reset(); - mArrowPath.setLastPoint(0, 0); - mArrowPath.lineTo(mArrowWidth / 2, -mArrowHeight); - mArrowPath.lineTo(mArrowWidth, 0); + 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.drawLine(0, 0, mArrowWidth / 2, -mArrowHeight, mArrowPaint); - canvas.drawLine(mArrowWidth / 2, -mArrowHeight, mArrowWidth, 0, mArrowPaint); + canvas.drawPath(mArrowPath, mArrowPaint); + mArrowPaint.setXfermode(mArrowAlignMode); + mArrowPaint.setStyle(Paint.Style.FILL); + canvas.drawRect(0, 0, mArrowWidth, mBorderWidth, mArrowPaint); + canvas.restoreToCount(saveLayer); } canvas.restore(); } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/popup/QMUIPopup.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/popup/QMUIPopup.java index cf2c829a7..0b9286968 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/popup/QMUIPopup.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/popup/QMUIPopup.java @@ -25,11 +25,20 @@ public class QMUIPopup extends QMUINormalPopup<QMUIPopup> { public QMUIPopup(Context context, int width, int height) { - super(context, width, 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); + } } \ No newline at end of file diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/popup/QMUIQuickAction.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/popup/QMUIQuickAction.java index 2c068c00e..98b2250c9 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/popup/QMUIQuickAction.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/popup/QMUIQuickAction.java @@ -18,6 +18,7 @@ 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; @@ -26,10 +27,19 @@ 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.QMUISkinManager; import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; import com.qmuiteam.qmui.util.QMUIResHelper; import com.qmuiteam.qmui.util.QMUIViewHelper; @@ -38,16 +48,6 @@ import java.util.ArrayList; import java.util.Objects; -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; - public class QMUIQuickAction extends QMUINormalPopup<QMUIQuickAction> { private ArrayList<Action> mActions = new ArrayList<>(); @@ -57,8 +57,13 @@ public class QMUIQuickAction extends QMUINormalPopup<QMUIQuickAction> { private int mMoreArrowWidth; private int mPaddingHor; - public QMUIQuickAction(Context context, int width, int height) { - super(context, width, height); + + 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); @@ -109,10 +114,16 @@ protected int proxyWidth(int width) { @Override public QMUIQuickAction show(@NonNull View anchor) { - view(createContentView()); 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); @@ -174,10 +185,10 @@ protected AppCompatImageView createMoreArrowView(boolean isLeft) { builder.tintColor(R.attr.qmui_skin_support_quick_action_more_tint_color); int bgColor = getBgColor(); int bgColorAttr = getBgColorAttr(); - if (bgColor != NOT_SET) { - arrowView.setBackgroundColor(bgColor); - } else if (bgColorAttr != 0) { + if (bgColorAttr != 0) { builder.background(bgColorAttr); + }else if (bgColor != Color.TRANSPARENT) { + arrowView.setBackgroundColor(bgColor); } QMUISkinHelper.setSkinValue(arrowView, builder); arrowView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/pullLayout/QMUIPullLayout.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/pullLayout/QMUIPullLayout.java index bd5fb4142..9f9ae229a 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/pullLayout/QMUIPullLayout.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/pullLayout/QMUIPullLayout.java @@ -1022,6 +1022,7 @@ public final static class PullAction { 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; @@ -1037,7 +1038,7 @@ public final static class PullAction { boolean isTargetCanOverPull, float targetPullRate, int actionInitOffset, - int pullEdge, + @PullEdge int pullEdge, float scrollSpeedPerPixel, boolean needReceiveFlingFromTargetView, float receivedFlingFraction, @@ -1107,6 +1108,7 @@ public boolean isCanOverPull() { return mCanOverPull; } + @PullEdge public int getPullEdge() { return mPullEdge; } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/pullRefreshLayout/QMUIPullRefreshLayout.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/pullRefreshLayout/QMUIPullRefreshLayout.java index a50d7c966..abfb39558 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/pullRefreshLayout/QMUIPullRefreshLayout.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/pullRefreshLayout/QMUIPullRefreshLayout.java @@ -18,7 +18,6 @@ import android.content.Context; import android.content.res.TypedArray; -import android.os.Build; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; @@ -30,15 +29,6 @@ import android.widget.AbsListView; import android.widget.Scroller; -import com.qmuiteam.qmui.BuildConfig; -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; - import androidx.annotation.ColorInt; import androidx.annotation.ColorRes; import androidx.annotation.Nullable; @@ -51,6 +41,15 @@ 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) * <ul> @@ -246,6 +245,10 @@ public void setAutoScrollToRefreshMinOffset(int autoScrollToRefreshMinOffset) { mAutoScrollToRefreshMinOffset = autoScrollToRefreshMinOffset; } + public boolean isRefreshing() { + return mIsRefreshing; + } + /** * 覆盖该方法以实现自己的 RefreshView。 * @@ -324,26 +327,11 @@ public void requestDisallowInterceptTouchEvent(boolean b) { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - ensureTargetView(); - if (mTargetView == null) { - Log.d(TAG, "onMeasure: mTargetView == null"); - return; - } - int targetMeasureWidthSpec = MeasureSpec.makeMeasureSpec( - getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY); - int targetMeasureHeightSpec = MeasureSpec.makeMeasureSpec( - getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY); - mTargetView.measure(targetMeasureWidthSpec, targetMeasureHeightSpec); + 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); - mRefreshZIndex = -1; - for (int i = 0; i < getChildCount(); i++) { - if (getChildAt(i) == mRefreshView) { - mRefreshZIndex = i; - break; - } - } - int refreshViewHeight = mRefreshView.getMeasuredHeight(); if (mAutoCalculateRefreshInitOffset) { if (mRefreshInitOffset != -refreshViewHeight) { @@ -358,6 +346,24 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 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 @@ -393,7 +399,7 @@ public boolean onInterceptTouchEvent(MotionEvent ev) { int pointerIndex; if (!isEnabled() || canChildScrollUp() || mNestedScrollInProgress) { - if (BuildConfig.DEBUG) { + if (QMUIConfig.DEBUG) { Log.d(TAG, "fast end onIntercept: isEnabled = " + isEnabled() + "; canChildScrollUp = " + canChildScrollUp() + " ; mNestedScrollInProgress = " + mNestedScrollInProgress); } @@ -473,9 +479,9 @@ public boolean onTouchEvent(MotionEvent ev) { if (mIsDragging) { float dy = (y - mLastMotionY) * mDragRate; if (dy >= 0) { - moveTargetView(dy, true); + moveTargetView(dy); } else { - int move = moveTargetView(dy, true); + int move = moveTargetView(dy); float delta = Math.abs(dy) - Math.abs(move); if (delta > 0) { // 重新dispatch一次down事件,使得列表可以继续滚动 @@ -574,7 +580,7 @@ private void finishPull(int vy) { " ; mTargetRefreshOffset = " + mTargetRefreshOffset + " ; mTargetInitOffset = " + mTargetInitOffset + " ; mScroller.isFinished() = " + mScroller.isFinished()); int miniVy = vy / 1000; // 向下拖拽时, 速度不能太大 - onFinishPull(miniVy, mRefreshInitOffset, mRefreshEndOffset, mRefreshView.getHeight(), + onFinishPull(miniVy, mRefreshInitOffset, mRefreshEndOffset, mRefreshView.getMeasuredHeight(), mTargetCurrentOffset, mTargetInitOffset, mTargetRefreshOffset); if (mTargetCurrentOffset >= mTargetRefreshOffset) { if (miniVy > 0) { @@ -662,26 +668,38 @@ public void finishRefresh() { } public void setToRefreshDirectly() { - setToRefreshDirectly(0); + setToRefreshDirectly(0, true); + } + + public void setToRefreshDirectly(final long delay){ + setToRefreshDirectly(delay, true); } - public void setToRefreshDirectly(final long delay) { + public void setToRefreshDirectly(final long delay, final boolean animate) { if (mTargetView != null) { - postDelayed(new Runnable() { + 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(); - mScrollFlag = FLAG_NEED_SCROLL_TO_REFRESH_POSITION; - invalidate(); } - }, delay); - + }; + if(delay == 0){ + runnable.run(); + }else{ + postDelayed(runnable, delay); + } } else { mPendingRefreshDirectlyAction = new Runnable() { @Override public void run() { - setToRefreshDirectly(delay); + setToRefreshDirectly(delay, animate); } }; } @@ -697,11 +715,7 @@ protected void setTargetViewToTop(View targetView) { ((RecyclerView) targetView).scrollToPosition(0); } else if (targetView instanceof AbsListView) { AbsListView listView = (AbsListView) targetView; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - listView.setSelectionFromTop(0, 0); - } else { - listView.setSelection(0); - } + listView.setSelectionFromTop(0, 0); } else { targetView.scrollTo(0, 0); } @@ -720,11 +734,11 @@ private void onSecondaryPointerUp(MotionEvent ev) { } public void reset() { - moveTargetViewTo(mTargetInitOffset, false); mIRefreshView.stop(); mIsRefreshing = false; mScroller.forceFinished(true); mScrollFlag = 0; + moveTargetViewTo(mTargetInitOffset); } protected void startDragging(float x, float y) { @@ -790,10 +804,10 @@ public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { if (dy > 0 && parentCanConsume > 0) { if (dy >= parentCanConsume) { consumed[1] = parentCanConsume; - moveTargetViewTo(mTargetInitOffset, true); + moveTargetViewTo(mTargetInitOffset); } else { consumed[1] = dy; - moveTargetView(-dy, true); + moveTargetView(-dy); } } } @@ -803,7 +817,7 @@ public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUn info("onNestedScroll: dxConsumed = " + dxConsumed + " ; dyConsumed = " + dyConsumed + " ; dxUnconsumed = " + dxUnconsumed + " ; dyUnconsumed = " + dyUnconsumed); if (dyUnconsumed < 0 && !canChildScrollUp() && mScroller.isFinished() && mScrollFlag == 0) { - moveTargetView(-dyUnconsumed, true); + moveTargetView(-dyUnconsumed); } } @@ -852,16 +866,16 @@ public boolean onNestedFling(View target, float velocityX, float velocityY, bool return false; } - private int moveTargetView(float dy, boolean isDragging) { + private int moveTargetView(float dy) { int target = (int) (mTargetCurrentOffset + dy); - return moveTargetViewTo(target, isDragging); + return moveTargetViewTo(target); } - private int moveTargetViewTo(int target, boolean isDragging) { - return moveTargetViewTo(target, isDragging, false); + private int moveTargetViewTo(int target) { + return moveTargetViewTo(target, false); } - private int moveTargetViewTo(int target, boolean isDragging, boolean calculateAnyWay) { + private int moveTargetViewTo(int target, boolean calculateAnyWay) { target = calculateTargetOffset(target, mTargetInitOffset, mTargetRefreshOffset, mEnableOverPull); int offset = 0; if (target != mTargetCurrentOffset || calculateAnyWay) { @@ -869,7 +883,7 @@ private int moveTargetViewTo(int target, boolean isDragging, boolean calculateAn ViewCompat.offsetTopAndBottom(mTargetView, offset); mTargetCurrentOffset = target; int total = mTargetRefreshOffset - mTargetInitOffset; - if (isDragging) { + if (!mIsRefreshing) { mIRefreshView.onPull(Math.min(mTargetCurrentOffset - mTargetInitOffset, total), total, mTargetCurrentOffset - mTargetRefreshOffset); } @@ -881,7 +895,7 @@ private int moveTargetViewTo(int target, boolean isDragging, boolean calculateAn if (mRefreshOffsetCalculator == null) { mRefreshOffsetCalculator = new QMUIDefaultRefreshOffsetCalculator(); } - int newRefreshOffset = mRefreshOffsetCalculator.calculateRefreshOffset(mRefreshInitOffset, mRefreshEndOffset, mRefreshView.getHeight(), + int newRefreshOffset = mRefreshOffsetCalculator.calculateRefreshOffset(mRefreshInitOffset, mRefreshEndOffset, mRefreshView.getMeasuredHeight(), mTargetCurrentOffset, mTargetInitOffset, mTargetRefreshOffset); if (newRefreshOffset != mRefreshCurrentOffset) { ViewCompat.offsetTopAndBottom(mRefreshView, newRefreshOffset - mRefreshCurrentOffset); @@ -964,7 +978,7 @@ private void removeFlag(int flag) { public void computeScroll() { if (mScroller.computeScrollOffset()) { int offsetY = mScroller.getCurrY(); - moveTargetViewTo(offsetY, false); + moveTargetViewTo(offsetY); if (offsetY <= 0 && hasFlag(FLAG_NEED_DELIVER_VELOCITY)) { deliverVelocity(); mScroller.forceFinished(true); @@ -981,13 +995,13 @@ public void computeScroll() { if (mTargetCurrentOffset != mTargetRefreshOffset) { mScroller.startScroll(0, mTargetCurrentOffset, 0, mTargetRefreshOffset - mTargetCurrentOffset); } else { - moveTargetViewTo(mTargetRefreshOffset, false, true); + moveTargetViewTo(mTargetRefreshOffset, true); } invalidate(); } else if (hasFlag(FLAG_NEED_DO_REFRESH)) { removeFlag(FLAG_NEED_DO_REFRESH); onRefresh(); - moveTargetViewTo(mTargetRefreshOffset, false, true); + moveTargetViewTo(mTargetRefreshOffset, true); } else { deliverVelocity(); } @@ -1009,7 +1023,7 @@ private void deliverVelocity() { } private void info(String msg) { - if (BuildConfig.DEBUG) { + if (QMUIConfig.DEBUG) { Log.i(TAG, msg); } } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/roundwidget/QMUIRoundButtonDrawable.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/roundwidget/QMUIRoundButtonDrawable.java index 1a4402db8..8144a1c4c 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/roundwidget/QMUIRoundButtonDrawable.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/roundwidget/QMUIRoundButtonDrawable.java @@ -19,13 +19,12 @@ import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; -import android.graphics.Color; import android.graphics.Rect; import android.graphics.drawable.GradientDrawable; -import android.os.Build; -import androidx.annotation.Nullable; import android.util.AttributeSet; +import androidx.annotation.Nullable; + import com.qmuiteam.qmui.R; /** @@ -51,18 +50,7 @@ public class QMUIRoundButtonDrawable extends GradientDrawable { * 设置按钮的背景色(只支持纯色,不支持 Bitmap 或 Drawable) */ public void setBgData(@Nullable ColorStateList colors) { - if (hasNativeStateListAPI()) { - super.setColor(colors); - } else { - mFillColors = colors; - final int currentColor; - if (colors == null) { - currentColor = Color.TRANSPARENT; - } else { - currentColor = colors.getColorForState(getState(), 0); - } - setColor(currentColor); - } + super.setColor(colors); } /** @@ -71,17 +59,7 @@ public void setBgData(@Nullable ColorStateList colors) { public void setStrokeData(int width, @Nullable ColorStateList colors) { mStrokeWidth = width; mStrokeColors = colors; - if (hasNativeStateListAPI()) { - super.setStroke(width, colors); - } else { - final int currentColor; - if (colors == null) { - currentColor = Color.TRANSPARENT; - } else { - currentColor = colors.getColorForState(getState(), 0); - } - setStroke(width, currentColor); - } + super.setStroke(width, colors); } public int getStrokeWidth(){ @@ -92,10 +70,6 @@ public void setStrokeColors(@Nullable ColorStateList colors){ setStrokeData(mStrokeWidth, colors); } - private boolean hasNativeStateListAPI() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; - } - /** * 设置圆角大小是否自动适应为 View 的高度的一半 */ diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/section/QMUIDefaultStickySectionAdapter.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/section/QMUIDefaultStickySectionAdapter.java index e0fc49939..9d392e80a 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/section/QMUIDefaultStickySectionAdapter.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/section/QMUIDefaultStickySectionAdapter.java @@ -26,6 +26,13 @@ public abstract class QMUIDefaultStickySectionAdapter< T extends QMUISection.Model<T>> extends QMUIStickySectionAdapter<H, T, QMUIStickySectionAdapter.ViewHolder> { + public QMUIDefaultStickySectionAdapter() { + } + + public QMUIDefaultStickySectionAdapter(boolean removeSectionTitleIfOnlyOneSection) { + super(removeSectionTitleIfOnlyOneSection); + } + @NonNull @Override protected ViewHolder onCreateSectionLoadingViewHolder(@NonNull ViewGroup viewGroup) { diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/section/QMUISectionDiffCallback.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/section/QMUISectionDiffCallback.java index c63316030..197ddc248 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/section/QMUISectionDiffCallback.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/section/QMUISectionDiffCallback.java @@ -22,6 +22,7 @@ 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; @@ -34,11 +35,12 @@ public class QMUISectionDiffCallback<H extends QMUISection.Model<H>, T extends Q private ArrayList<QMUISection<H, T>> mOldList = new ArrayList<>(); private ArrayList<QMUISection<H, T>> mNewList = new ArrayList<>(); - private SparseIntArray mOldSectionIndex = new SparseIntArray(); - private SparseIntArray mOldItemIndex = new SparseIntArray(); + private ArrayList<Integer> mOldSectionIndex = new ArrayList<>(); + private ArrayList<Integer> mOldItemIndex = new ArrayList<>(); - private SparseIntArray mNewSectionIndex = new SparseIntArray(); - private SparseIntArray mNewItemIndex = new SparseIntArray(); + private ArrayList<Integer> mNewSectionIndex = new ArrayList<>(); + private ArrayList<Integer> mNewItemIndex = new ArrayList<>(); + private boolean mRemoveSectionTitleIfOnlyOnceSection; public QMUISectionDiffCallback( @Nullable List<QMUISection<H, T>> oldList, @@ -50,24 +52,30 @@ public QMUISectionDiffCallback( if (newList != null) { mNewList.addAll(newList); } + } - generateIndex(mOldList, mOldSectionIndex, mOldItemIndex); - generateIndex(mNewList, mNewSectionIndex, mNewItemIndex); + void generateIndex(boolean removeSectionTitleIfOnlyOnceSection){ + mRemoveSectionTitleIfOnlyOnceSection = removeSectionTitleIfOnlyOnceSection; + generateIndex(mOldList, mOldSectionIndex, mOldItemIndex, removeSectionTitleIfOnlyOnceSection); + generateIndex(mNewList, mNewSectionIndex, mNewItemIndex, removeSectionTitleIfOnlyOnceSection); } - public void cloneNewIndexTo(@NonNull SparseIntArray sectionIndex, @NonNull SparseIntArray itemIndex) { + public void cloneNewIndexTo(@NonNull ArrayList<Integer> sectionIndex, @NonNull ArrayList<Integer> itemIndex) { sectionIndex.clear(); itemIndex.clear(); + sectionIndex.ensureCapacity(mNewSectionIndex.size()); + itemIndex.ensureCapacity(mNewItemIndex.size()); for (int i = 0; i < mNewSectionIndex.size(); i++) { - sectionIndex.append(mNewSectionIndex.keyAt(i), mNewSectionIndex.valueAt(i)); + sectionIndex.add(i, mNewSectionIndex.get(i)); } for (int i = 0; i < mNewItemIndex.size(); i++) { - itemIndex.append(mNewItemIndex.keyAt(i), mNewItemIndex.valueAt(i)); + itemIndex.add(i, mNewItemIndex.get(i)); } } private void generateIndex(List<QMUISection<H, T>> list, - SparseIntArray sectionIndex, SparseIntArray itemIndex) { + ArrayList<Integer> sectionIndex, ArrayList<Integer> itemIndex, + boolean removeSectionTitleIfOnlyOnceSection) { sectionIndex.clear(); itemIndex.clear(); IndexGenerationInfo generationInfo = new IndexGenerationInfo(sectionIndex, itemIndex); @@ -80,7 +88,9 @@ private void generateIndex(List<QMUISection<H, T>> list, if (section.isLocked()) { continue; } - generationInfo.appendIndex(i, ITEM_INDEX_SECTION_HEADER); + if(!removeSectionTitleIfOnlyOnceSection || list.size() > 1){ + generationInfo.appendIndex(i, ITEM_INDEX_SECTION_HEADER); + } if (section.isFold()) { continue; } @@ -225,6 +235,16 @@ public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { 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<H, T> oldModel = mOldList.get(oldSectionIndex); QMUISection<H, T> newModel = mNewList.get(newSectionIndex); @@ -251,14 +271,12 @@ public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { } public static class IndexGenerationInfo { - private SparseIntArray sectionIndexArray; - private SparseIntArray itemIndexArray; - private int currentPosition; + private ArrayList<Integer> sectionIndexArray; + private ArrayList<Integer> itemIndexArray; - private IndexGenerationInfo(SparseIntArray sectionIndex, SparseIntArray itemIndex) { + private IndexGenerationInfo(ArrayList<Integer> sectionIndex, ArrayList<Integer> itemIndex) { sectionIndexArray = sectionIndex; itemIndexArray = itemIndex; - currentPosition = 0; } public final void appendCustomIndex(int sectionIndex, int itemIndex) { @@ -271,13 +289,12 @@ public final void appendCustomIndex(int sectionIndex, int itemIndex) { appendIndex(sectionIndex, offset); } - private final void appendIndex(int sectionIndex, int itemIndex) { + private void appendIndex(int sectionIndex, int itemIndex) { if (sectionIndex < 0) { throw new IllegalArgumentException("use appendWholeListCustomIndex for whole list"); } - sectionIndexArray.append(currentPosition, sectionIndex); - itemIndexArray.append(currentPosition, itemIndex); - currentPosition++; + sectionIndexArray.add(sectionIndex); + itemIndexArray.add(itemIndex); } public final void appendWholeListCustomIndex(int itemIndex) { @@ -289,10 +306,9 @@ public final void appendWholeListCustomIndex(int itemIndex) { appendWholeListIndex(offset); } - private final void appendWholeListIndex(int itemIndex) { - sectionIndexArray.append(currentPosition, QMUISection.SECTION_INDEX_UNKNOWN); - itemIndexArray.append(currentPosition, itemIndex); - currentPosition++; + private void appendWholeListIndex(int itemIndex) { + sectionIndexArray.add(QMUISection.SECTION_INDEX_UNKNOWN); + itemIndexArray.add(itemIndex); } } } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/section/QMUIStickySectionAdapter.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/section/QMUIStickySectionAdapter.java index 5ae6375fa..e973f282d 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/section/QMUIStickySectionAdapter.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/section/QMUIStickySectionAdapter.java @@ -45,13 +45,23 @@ public abstract class QMUIStickySectionAdapter< private List<QMUISection<H, T>> mBackupData = new ArrayList<>(); private List<QMUISection<H, T>> mCurrentData = new ArrayList<>(); - private SparseIntArray mSectionIndex = new SparseIntArray(); - private SparseIntArray mItemIndex = new SparseIntArray(); + private ArrayList<Integer> mSectionIndex = new ArrayList<>(); + private ArrayList<Integer> mItemIndex = new ArrayList<>(); private ArrayList<QMUISection<H, T>> mLoadingBeforeSections = new ArrayList<>(2); private ArrayList<QMUISection<H, T>> mLoadingAfterSections = new ArrayList<>(2); private Callback<H, T> mCallback; private ViewCallback mViewCallback; + private final boolean mRemoveSectionTitleIfOnlyOneSection; + + + public QMUIStickySectionAdapter() { + this(false); + } + + public QMUIStickySectionAdapter(boolean removeSectionTitleIfOnlyOneSection) { + mRemoveSectionTitleIfOnlyOneSection = removeSectionTitleIfOnlyOneSection; + } /** * see {@link #setData(List, boolean, boolean)} @@ -142,6 +152,7 @@ public final void setDataWithoutDiff(@Nullable List<QMUISection<H, T>> data, boo } // only used to generate index info QMUISectionDiffCallback callback = createDiffCallback(mBackupData, mCurrentData); + callback.generateIndex(mRemoveSectionTitleIfOnlyOneSection); callback.cloneNewIndexTo(mSectionIndex, mItemIndex); notifyDataSetChanged(); mBackupData.clear(); @@ -152,6 +163,7 @@ public final void setDataWithoutDiff(@Nullable List<QMUISection<H, T>> data, boo 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); @@ -175,13 +187,15 @@ private void diff(boolean newDataSet, boolean onlyMutateState) { */ 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<H, T> createDiffCallback( - List<QMUISection<H, T>> lastData, List<QMUISection<H, T>> currentData) { + List<QMUISection<H, T>> lastData, + List<QMUISection<H, T>> currentData) { return new QMUISectionDiffCallback<>(lastData, currentData); } @@ -194,6 +208,10 @@ void setViewCallback(ViewCallback viewCallback) { } + public int getSectionCount() { + return mCurrentData.size(); + } + public int getItemIndex(int position) { if (position < 0 || position >= mItemIndex.size()) { return QMUISection.ITEM_INDEX_UNKNOWN; @@ -259,7 +277,7 @@ public void finishLoadMore(QMUISection<H, T> section, List<T> itemList, mLoadingAfterSections.remove(section); } - if (mCurrentData.indexOf(section) < 0) { + if (!mCurrentData.contains(section)) { return; } @@ -267,11 +285,10 @@ public void finishLoadMore(QMUISection<H, T> section, List<T> itemList, // wash current items down if (isLoadBefore && !section.isFold()) { for (int i = 0; i < mItemIndex.size(); i++) { - int position = mItemIndex.keyAt(i); - int itemIndex = mItemIndex.valueAt(i); - if (itemIndex == 0 && section == getSection(position)) { + int itemIndex = mItemIndex.get(i); + if (itemIndex == 0 && section == getSection(i)) { RecyclerView.ViewHolder focusViewHolder = mViewCallback == null ? null : - mViewCallback.findViewHolderForAdapterPosition(position); + mViewCallback.findViewHolderForAdapterPosition(i); if (focusViewHolder != null) { mViewCallback.requestChildFocus(focusViewHolder.itemView); } @@ -365,16 +382,15 @@ public void scrollToSectionHeader(@NonNull QMUISection<H, T> targetSection, bool private void safeScrollToSection(@NonNull QMUISection<H, T> targetSection, boolean scrollToTop) { for (int i = 0; i < mSectionIndex.size(); i++) { - int position = mSectionIndex.keyAt(i); - int sectionIndex = mSectionIndex.valueAt(i); + int sectionIndex = mSectionIndex.get(i); if (sectionIndex < 0 || sectionIndex >= mCurrentData.size()) { continue; } - int itemIndex = mItemIndex.get(position); + int itemIndex = mItemIndex.get(i); if (itemIndex == ITEM_INDEX_SECTION_HEADER) { QMUISection<H, T> temp = mCurrentData.get(sectionIndex); if (temp.getHeader().isSameItem(targetSection.getHeader())) { - mViewCallback.scrollToPosition(position, true, scrollToTop); + mViewCallback.scrollToPosition(i, true, scrollToTop); return; } } @@ -415,17 +431,16 @@ public void scrollToSectionItem(@Nullable QMUISection<H, T> targetSection, @NonN private void safeScrollToSectionItem(@NonNull QMUISection<H, T> targetSection, @NonNull T item, boolean scrollToTop) { for (int i = 0; i < mItemIndex.size(); i++) { - int position = mItemIndex.keyAt(i); - int itemIndex = mItemIndex.valueAt(i); + int itemIndex = mItemIndex.get(i); if (itemIndex < 0) { continue; } - QMUISection<H, T> section = getSection(position); + QMUISection<H, T> section = getSection(i); if (section != targetSection) { continue; } if (section.getItemAt(itemIndex).isSameItem(item)) { - mViewCallback.scrollToPosition(position, false, scrollToTop); + mViewCallback.scrollToPosition(i, false, scrollToTop); return; } } @@ -549,10 +564,9 @@ public void toggleFold(int position, boolean scrollToTop) { diff(false, true); if (scrollToTop && !section.isFold() && mViewCallback != null) { for (int i = 0; i < mSectionIndex.size(); i++) { - int pos = mSectionIndex.keyAt(i); - int itemIndex = getItemIndex(pos); - if (itemIndex == ITEM_INDEX_SECTION_HEADER && getSection(pos) == section) { - mViewCallback.scrollToPosition(pos, true, true); + int itemIndex = getItemIndex(i); + if (itemIndex == ITEM_INDEX_SECTION_HEADER && getSection(i) == section) { + mViewCallback.scrollToPosition(i, true, true); return; } } @@ -570,6 +584,10 @@ public int getRelativeStickyPosition(int position) { return position; } + public boolean isRemoveSectionTitleIfOnlyOneSection() { + return mRemoveSectionTitleIfOnlyOneSection; + } + @Override public final int getItemCount() { return mItemIndex.size(); diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/section/QMUIStickySectionItemDecoration.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/section/QMUIStickySectionItemDecoration.java index da6e42753..c0ab860e5 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/section/QMUIStickySectionItemDecoration.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/section/QMUIStickySectionItemDecoration.java @@ -115,12 +115,17 @@ public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, ViewGroup sectionContainer = mWeakSectionContainer.get(); - if (sectionContainer == null || parent.getChildCount() == 0) { + if (sectionContainer == null) { return; } + if(parent.getChildCount() == 0){ + setHeaderVisibility(false); + } + RecyclerView.Adapter adapter = parent.getAdapter(); if (adapter == null) { + setHeaderVisibility(false); return; } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUIBasicTabSegment.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUIBasicTabSegment.java index 76fa9787f..8cc4e19a4 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUIBasicTabSegment.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUIBasicTabSegment.java @@ -268,6 +268,18 @@ public void reset() { } } + /** + * clear select info + */ + public void resetSelect() { + mCurrentSelectedIndex = NO_POSITION; + mPendingSelectedIndex = NO_POSITION; + if (mSelectAnimator != null) { + mSelectAnimator.cancel(); + mSelectAnimator = null; + } + } + /** * add a tab to QMUITabSegment @@ -285,9 +297,16 @@ public QMUIBasicTabSegment addTab(QMUITab tab) { * 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); @@ -317,17 +336,21 @@ public void setMode(@Mode int mode) { } - void onClickTab(int index) { + 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); } - if (mOnTabClickListener != null) { - mOnTabClickListener.onTabClick(index); - } } protected boolean needPreventEvent() { @@ -423,7 +446,11 @@ public void selectTab(final int index, boolean noAnimation, boolean fromTabClick if (mCurrentSelectedIndex == NO_POSITION) { QMUITab model = mTabAdapter.getItem(index); layoutIndicator(model, true); - listViews.get(index).setSelectFraction(1f); + + QMUITabView tabView = listViews.get(index); + tabView.setSelected(true); // 标记选中,使得TalkBack等屏幕阅读器可向用户报告tab状态 + tabView.setSelectFraction(1f); + dispatchTabSelected(index); mCurrentSelectedIndex = index; mIsInSelectTab = false; @@ -440,7 +467,9 @@ public void selectTab(final int index, boolean noAnimation, boolean fromTabClick 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(), @@ -499,12 +528,15 @@ public void onAnimationStart(Animator animation) { @Override public void onAnimationEnd(Animator animation) { - mSelectAnimator = null; 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); - mCurrentSelectedIndex = index; if (mPendingSelectedIndex != NO_POSITION && !needPreventEvent()) { selectTab(mPendingSelectedIndex, true, false); mPendingSelectedIndex = NO_POSITION; @@ -515,7 +547,9 @@ public void onAnimationEnd(Animator animation) { 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); } @@ -711,9 +745,12 @@ public interface OnTabClickListener { /** * 当某个 Tab 被点击时会触发 * + * @param tabView 被点击的View * @param index 被点击的 Tab 下标 + * + * @return true 拦截 selectTab 事件 */ - void onTabClick(int index); + boolean onTabClick(QMUITabView tabView, int index); } public interface OnTabSelectedListener { @@ -769,6 +806,7 @@ private final class Container extends ViewGroup { public Container(Context context) { super(context); setClipChildren(false); + setWillNotDraw(false); } @Override @@ -906,8 +944,8 @@ protected void onLayout(boolean changed, int l, int t, int r, int b) { } @Override - protected void dispatchDraw(Canvas canvas) { - super.dispatchDraw(canvas); + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); if (mIndicator != null && (!mHideIndicatorWhenTabCountLessTwo || mTabAdapter.getSize() > 1)) { mIndicator.draw(this, canvas, getPaddingTop(), getHeight() - getPaddingBottom()); } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUITab.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUITab.java index acb07ff30..ce136e2bc 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUITab.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUITab.java @@ -20,26 +20,27 @@ import android.view.Gravity; import android.view.View; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -import androidx.annotation.ColorInt; 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; - public static final int SIGN_COUNT_VIEW_LAYOUT_VERTICAL_CENTER = Integer.MIN_VALUE; - @IntDef(value = { ICON_POSITION_LEFT, ICON_POSITION_TOP, @@ -56,6 +57,7 @@ public class QMUITab { int selectedTextSize; Typeface normalTypeface; Typeface selectedTypeface; + float typefaceUpdateAreaPercent; int normalColor; int selectColor; int normalColorAttr; @@ -65,6 +67,8 @@ public class QMUITab { float selectedTabIconScale = 1f; QMUITabIcon tabIcon = null; boolean skinChangeWithTintColor; + boolean skinChangeNormalWithTintColor; + boolean skinChangeSelectedWithTintColor; int normalIconAttr; int selectedIconAttr; int contentWidth = 0; @@ -72,9 +76,11 @@ public class QMUITab { @IconPosition int iconPosition = ICON_POSITION_TOP; int gravity = Gravity.CENTER; private CharSequence text; + private CharSequence description; int signCountDigits = 2; - int signCountLeftMarginWithIconOrText = 0; - int signCountBottomMarginWithIconOrText = 0; + 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; @@ -84,7 +90,12 @@ public class QMUITab { QMUITab(CharSequence text) { + this(text, text); + } + + QMUITab(CharSequence text, CharSequence description) { this.text = text; + this.description = description; } @@ -96,6 +107,14 @@ 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; } @@ -109,6 +128,13 @@ public void setSpaceWeight(float leftWeight, float rightWeight) { rightSpaceWeight = rightWeight; } + public void setTypefaceUpdateAreaPercent(float typefaceUpdateAreaPercent) { + this.typefaceUpdateAreaPercent = typefaceUpdateAreaPercent; + } + + public float getTypefaceUpdateAreaPercent() { + return typefaceUpdateAreaPercent; + } public int getGravity() { return gravity; diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUITabAdapter.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUITabAdapter.java index 663a29266..f9b74087b 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUITabAdapter.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUITabAdapter.java @@ -37,6 +37,18 @@ protected QMUITabView createView(ViewGroup parentView) { 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) { @@ -46,7 +58,7 @@ protected void onBindTab(QMUITab item, QMUITabView view, int position) { @Override public void onClick(QMUITabView view) { int index = getViews().indexOf(view); - mTabSegment.onClickTab(index); + mTabSegment.onClickTab(view, index); } @Override diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUITabBuilder.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUITabBuilder.java index 7955ea95b..0e7e57c29 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUITabBuilder.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUITabBuilder.java @@ -20,12 +20,12 @@ 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; -import androidx.annotation.Nullable; - /** * use {@link QMUITabSegment#tabBuilder()} to get a instance @@ -48,9 +48,11 @@ public class QMUITabBuilder { /** * for skin change. if true, then normalDrawableAttr and selectedDrawableAttr will not work. - * otherwise, icon will be replaced by normalDrawableAttr and selectedDrawable + * otherwise, icon will be replaced by normalDrawableAttr and selectedDrawableAttr */ - private boolean skinChangeWithTintColor = true; + private boolean skinChangeWithTintColor = false; + private boolean skinChangeNormalWithTintColor = true; + private boolean skinChangeSelectedWithTintColor = true; /** * text size in normal state @@ -88,10 +90,9 @@ public class QMUITabBuilder { * gravity of text */ private int gravity = Gravity.CENTER; - /** - * text - */ + private CharSequence text; + private CharSequence description; /** * text typeface in normal state @@ -116,6 +117,8 @@ public class QMUITabBuilder { */ float selectedTabIconScale = 1f; + float typefaceUpdateAreaPercent = 0.25f; + /** * signCount or redPoint */ @@ -127,13 +130,15 @@ public class QMUITabBuilder { */ private int signCountDigits = 2; /** - * the margin left of signCount(redPoint) view + * the horizontal offset of signCount(redPoint) view */ - private int signCountLeftMarginWithIconOrText; + private int signCountHorizontalOffset; /** - * the margin top of signCount(redPoint) view + * the vertical offset of signCount(redPoint) view */ - private int signCountBottomMarginWithIconOrText; + private int signCountVerticalOffset; + + private int signCountVerticalAlign = QMUITab.SIGN_COUNT_VERTICAL_ALIGN_BOTTOM_TO_CONTENT_TOP; /** * the gap between icon and text @@ -149,8 +154,8 @@ public class QMUITabBuilder { QMUITabBuilder(Context context) { iconTextGap = QMUIDisplayHelper.dp2px(context, 2); normalTextSize = selectTextSize = QMUIDisplayHelper.dp2px(context, 12); - signCountLeftMarginWithIconOrText = QMUIDisplayHelper.dp2px(context, 3); - signCountBottomMarginWithIconOrText = signCountLeftMarginWithIconOrText; + signCountHorizontalOffset = QMUIDisplayHelper.dp2px(context, 3); + signCountVerticalOffset = signCountHorizontalOffset; } QMUITabBuilder(QMUITabBuilder other) { @@ -166,10 +171,12 @@ public class QMUITabBuilder { 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.signCountLeftMarginWithIconOrText = other.signCountLeftMarginWithIconOrText; - this.signCountBottomMarginWithIconOrText = other.signCountBottomMarginWithIconOrText; + this.signCountHorizontalOffset = other.signCountHorizontalOffset; + this.signCountVerticalOffset = other.signCountVerticalOffset; + this.signCountVerticalAlign = other.signCountVerticalAlign; this.normalTypeface = other.normalTypeface; this.selectedTypeface = other.selectedTypeface; this.normalTabIconWidth = other.normalTabIconWidth; @@ -177,6 +184,12 @@ public class QMUITabBuilder { 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) { @@ -184,6 +197,11 @@ public QMUITabBuilder setAllowIconDrawOutside(boolean allowIconDrawOutside) { return this; } + public QMUITabBuilder setTypefaceUpdateAreaPercent(float typefaceUpdateAreaPercent) { + this.typefaceUpdateAreaPercent = typefaceUpdateAreaPercent; + return this; + } + public QMUITabBuilder setNormalDrawable(Drawable normalDrawable) { this.normalDrawable = normalDrawable; return this; @@ -204,11 +222,21 @@ public QMUITabBuilder setSelectedDrawableAttr(int 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; @@ -244,10 +272,22 @@ public QMUITabBuilder setSignCount(int signCount) { } public QMUITabBuilder setSignCountMarginInfo(int digit, - int leftMarginWithIconOrText, int bottomMarginWithIconOrText) { + 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.signCountLeftMarginWithIconOrText = leftMarginWithIconOrText; - this.signCountBottomMarginWithIconOrText = bottomMarginWithIconOrText; + this.signCountHorizontalOffset = horizontalOffset; + this.signCountVerticalOffset = verticalOffset; + this.signCountVerticalAlign = verticalAlign; return this; } @@ -307,27 +347,41 @@ public QMUITabBuilder setText(CharSequence text) { return this; } + public QMUITabBuilder setDescription(CharSequence description){ + this.description = description; + return this; + } + public QMUITab build(Context context) { - QMUITab tab = new QMUITab(this.text); + QMUITab tab = new QMUITab(text, description == null ? text : description); if(!skinChangeWithTintColor){ - if(normalDrawableAttr != 0){ - normalDrawable = QMUIResHelper.getAttrDrawable(context, normalDrawableAttr); + if(!skinChangeNormalWithTintColor){ + if(normalDrawableAttr != 0){ + normalDrawable = QMUIResHelper.getAttrDrawable(context, normalDrawableAttr); + } } - if(selectedDrawableAttr != 0){ - selectedDrawable = QMUIResHelper.getAttrDrawable(context, selectedDrawableAttr); + 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, dynamicChangeIconColor); + 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.skinChangeWithTintColor = this.skinChangeWithTintColor; tab.normalIconAttr = this.normalDrawableAttr; tab.selectedIconAttr = this.selectedDrawableAttr; tab.normalTabIconWidth = this.normalTabIconWidth; @@ -345,9 +399,11 @@ public QMUITab build(Context context) { tab.selectColor = this.selectColor; tab.signCount = this.signCount; tab.signCountDigits = this.signCountDigits; - tab.signCountLeftMarginWithIconOrText = this.signCountLeftMarginWithIconOrText; - tab.signCountBottomMarginWithIconOrText = this.signCountBottomMarginWithIconOrText; + tab.signCountHorizontalOffset = this.signCountHorizontalOffset; + tab.signCountVerticalAlign = this.signCountVerticalAlign; + tab.signCountVerticalOffset = this.signCountVerticalOffset; tab.iconTextGap = this.iconTextGap; + tab.typefaceUpdateAreaPercent = this.typefaceUpdateAreaPercent; return tab; } } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUITabIcon.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUITabIcon.java index 9d9326610..869c582ee 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUITabIcon.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUITabIcon.java @@ -75,6 +75,39 @@ public void tint(int normalColor, int 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); diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUITabSegment.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUITabSegment.java index 061d6c1d5..f41b508e8 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUITabSegment.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUITabSegment.java @@ -72,6 +72,10 @@ public void notifyDataChanged() { populateFromPagerAdapter(false); } + public void notifyDataRefreshed(){ + super.notifyDataChanged(); + } + public void setupWithViewPager(@Nullable ViewPager viewPager) { setupWithViewPager(viewPager, true); } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUITabView.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUITabView.java index f5266c7c4..cb3caba72 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUITabView.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUITabView.java @@ -27,10 +27,16 @@ 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 com.qmuiteam.qmui.QMUILog; +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; @@ -43,12 +49,6 @@ import com.qmuiteam.qmui.util.QMUIResHelper; import com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.collection.SimpleArrayMap; -import androidx.core.view.GravityCompat; -import androidx.core.view.ViewCompat; - import org.jetbrains.annotations.NotNull; public class QMUITabView extends FrameLayout implements IQMUISkinHandlerView { @@ -76,10 +76,18 @@ public class QMUITabView extends FrameLayout implements IQMUISkinHandlerView { 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() { @@ -133,6 +141,7 @@ public boolean onTouchEvent(MotionEvent 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()); @@ -151,6 +160,7 @@ public void bind(QMUITab tab) { 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 { @@ -169,10 +179,17 @@ public void bind(QMUITab tab) { } 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, @@ -315,7 +332,7 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if(mSignCountView != null && mSignCountView.getVisibility() != View.GONE){ mSignCountView.measure(0, 0); widthSize = Math.max(widthSize, - widthSize + mSignCountView.getMeasuredWidth() + mTab.signCountLeftMarginWithIconOrText); + widthSize + mSignCountView.getMeasuredWidth() + mTab.signCountHorizontalOffset); } useWidthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); } @@ -380,23 +397,26 @@ protected void onLayoutSignCount(int width, int height) { private Point calculateSignCountLayoutPosition() { QMUITabIcon icon = mTab.getTabIcon(); - int left, bottom; + int anchorLeft, anchorTop; int iconPosition = mTab.getIconPosition(); if (icon == null || iconPosition == QMUITab.ICON_POSITION_BOTTOM || iconPosition == QMUITab.ICON_POSITION_LEFT) { - left = (int) (mCurrentTextLeft + mCurrentTextWidth); - bottom = (int) (mCurrentTextTop); + anchorLeft = (int) (mCurrentTextLeft + mCurrentTextWidth); + anchorTop = (int) (mCurrentTextTop); } else { - left = (int) (mCurrentIconLeft + mCurrentIconWidth); - bottom = (int) (mCurrentIconTop); + anchorLeft = (int) (mCurrentIconLeft + mCurrentIconWidth); + anchorTop = (int) (mCurrentIconTop); } - Point point = new Point(left, bottom); - int verticalOffset = mTab.signCountBottomMarginWithIconOrText; - if(verticalOffset == QMUITab.SIGN_COUNT_VIEW_LAYOUT_VERTICAL_CENTER && mSignCountView != null){ + 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.signCountLeftMarginWithIconOrText, 0); - }else{ - point.offset(mTab.signCountLeftMarginWithIconOrText, verticalOffset); + point.offset(mTab.signCountHorizontalOffset, verticalOffset); + }else { + point.offset(mTab.signCountHorizontalOffset, verticalOffset); } @@ -666,6 +686,16 @@ 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) { @@ -702,24 +732,42 @@ private void updateSkinInfo(QMUITab tab) { ColorStateList.valueOf(selectedColor), true); if (tab.tabIcon != null) { - if (tab.skinChangeWithTintColor) { + if (tab.skinChangeWithTintColor || (tab.skinChangeNormalWithTintColor && tab.skinChangeSelectedWithTintColor)) { tab.tabIcon.tint(normalColor, selectedColor); } else { - Drawable normalIcon = null; - Drawable selectedIcon = null; - if (tab.normalIconAttr != 0) { - normalIcon = QMUISkinHelper.getSkinDrawable(this, tab.normalIconAttr); - } + 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.selectedIconAttr != 0) { - selectedIcon = QMUISkinHelper.getSkinDrawable(this, tab.selectedIconAttr); - } - if (normalIcon != null && selectedIcon != null) { - tab.tabIcon.src(normalIcon, selectedIcon); - } else if (normalIcon != null && !tab.tabIcon.hasSelectedIcon()) { - tab.tabIcon.src(normalIcon, normalColor, selectedColor); - } else { - QMUILog.i(TAG, "skin attr not matched with current value."); + 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); + } + } + } } } } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/textview/QMUISpanTouchFixTextView.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/textview/QMUISpanTouchFixTextView.java index b55fc6522..63b05bb53 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/textview/QMUISpanTouchFixTextView.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/textview/QMUISpanTouchFixTextView.java @@ -25,14 +25,14 @@ 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; -import androidx.annotation.ColorInt; -import androidx.appcompat.widget.AppCompatTextView; - /** * <p> * 修复了 {@link TextView} 与 {@link android.text.style.ClickableSpan} 一起使用时, @@ -111,7 +111,8 @@ public void setMovementMethodCompat(MovementMethod movement){ @Override public boolean onTouchEvent(MotionEvent event) { - if (!(getText() instanceof Spannable)) { + if (!(getText() instanceof Spannable) || !(getMovementMethod() instanceof QMUILinkTouchMovementMethod)) { + mTouchSpanHit = false; return super.onTouchEvent(event); } mTouchSpanHit = true; diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/webview/QMUIBridgeWebViewClient.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/webview/QMUIBridgeWebViewClient.java index f4703ec3e..c215134f8 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/webview/QMUIBridgeWebViewClient.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/webview/QMUIBridgeWebViewClient.java @@ -6,25 +6,34 @@ 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; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - 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; } @@ -63,11 +72,16 @@ protected boolean onShouldOverrideUrlLoading(WebView view, WebResourceRequest re @Override public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); - String bridgeScript = loadBridgeScript(view.getContext()); - if (bridgeScript != null) { - view.evaluateJavascript(bridgeScript, null); + if(mNeedInjectLocalBridgeJs){ + String bridgeScript = loadBridgeScript(view.getContext()); + if (bridgeScript != null) { + view.evaluateJavascript(bridgeScript, null); + mWebViewBridgeHandler.onBridgeLoaded(); + } + }else{ mWebViewBridgeHandler.onBridgeLoaded(); } + } @Nullable diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/webview/QMUIWebView.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/webview/QMUIWebView.java index 5e997ba9c..4ae19eb90 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/webview/QMUIWebView.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/webview/QMUIWebView.java @@ -19,27 +19,27 @@ import android.content.Context; import android.graphics.Rect; import android.os.Build; -import androidx.annotation.NonNull; -import androidx.core.view.ViewCompat; -import androidx.core.view.WindowInsetsCompat; import android.util.AttributeSet; import android.util.Log; import android.view.KeyEvent; -import android.view.WindowInsets; +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.QMUINotchHelper; import com.qmuiteam.qmui.util.QMUIWindowInsetHelper; -import com.qmuiteam.qmui.widget.IWindowInsetLayout; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; -public class QMUIWebView extends WebView implements IWindowInsetLayout { +public class QMUIWebView extends WebView { private static final String TAG = "QMUIWebView"; private static boolean sIsReflectionOccurError = false; @@ -55,8 +55,6 @@ public class QMUIWebView extends WebView implements IWindowInsetLayout { private boolean mNeedDispatchSafeAreaInset = false; private Callback mCallback; private List<OnScrollChangeListener> mOnScrollChangeListeners = new ArrayList<>(); - private QMUIWindowInsetHelper mWindowInsetHelper; - public QMUIWebView(Context context) { super(context); @@ -77,7 +75,21 @@ private void init() { removeJavascriptInterface("searchBoxJavaBridge_"); removeJavascriptInterface("accessibility"); removeJavascriptInterface("accessibilityTraversal"); - mWindowInsetHelper = new QMUIWindowInsetHelper(this, this); + 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 @@ -158,41 +170,6 @@ boolean isNotSupportChangeCssEnv() { return sIsReflectionOccurError; } - @Override - public boolean applySystemWindowInsets19(Rect insets) { - return false; - } - - @Override - public boolean applySystemWindowInsets21(Object insets) { - if (!mNeedDispatchSafeAreaInset) { - return false; - } - float density = QMUIDisplayHelper.getDensity(getContext()); - int left, top, right, bottom; - if (QMUINotchHelper.isNotchOfficialSupport()) { - WindowInsets windowInsets = (WindowInsets) insets; - left = windowInsets.getSystemWindowInsetLeft(); - top = windowInsets.getSystemWindowInsetTop(); - right = windowInsets.getSystemWindowInsetRight(); - bottom = windowInsets.getSystemWindowInsetBottom(); - } else { - WindowInsetsCompat insetsCompat = (WindowInsetsCompat) insets; - left = insetsCompat.getSystemWindowInsetLeft(); - top = insetsCompat.getSystemWindowInsetTop(); - right = insetsCompat.getSystemWindowInsetRight(); - bottom = insetsCompat.getSystemWindowInsetBottom(); - } - Rect rect = new Rect( - (int) (left / density + getExtraInsetLeft(density)), - (int) (top / density + getExtraInsetTop(density)), - (int) (right / density + getExtraInsetRight(density)), - (int) (bottom / density + getExtraInsetBottom(density)) - ); - setStyleDisplayCutoutSafeArea(rect); - return true; - } - protected int getExtraInsetTop(float density) { return 0; } @@ -330,11 +307,8 @@ private Object getWebContentsFieldValueInAwContents(Object awContents) throws Il private Method getSetDisplayCutoutSafeAreaMethodInWebContents(Object webContents) { try { - Method setDisplayCutoutSafeAreaMethod = webContents.getClass() + return webContents.getClass() .getDeclaredMethod("setDisplayCutoutSafeArea", Rect.class); - if (setDisplayCutoutSafeAreaMethod != null) { - return setDisplayCutoutSafeAreaMethod; - } } catch (NoSuchMethodException ignored) { } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/webview/QMUIWebViewBridgeHandler.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/webview/QMUIWebViewBridgeHandler.java index bed301838..5700e3c82 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/webview/QMUIWebViewBridgeHandler.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/webview/QMUIWebViewBridgeHandler.java @@ -4,6 +4,9 @@ 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; @@ -11,16 +14,14 @@ import java.util.ArrayList; import java.util.List; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - 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_RESPONSE_ID = "id"; + private static final String MESSAGE_INNER_CMD_NAME = "__cmd__"; + private static final String MESSAGE_CMD_GET_SUPPORTED_CMD_LIST = "getSupportedCmdList"; private List<Pair<String, ValueCallback<String>>> mStartupMessageList = new ArrayList<>(); private WebView mWebView; @@ -40,7 +41,7 @@ public final void evaluateBridgeScript(String script, ValueCallback<String> resu void onBridgeLoaded() { if (mStartupMessageList != null) { for (Pair<String, ValueCallback<String>> message : mStartupMessageList) { - mWebView.evaluateJavascript("", null); + mWebView.evaluateJavascript(message.first, message.second); } mStartupMessageList = null; } @@ -58,14 +59,27 @@ public void onReceiveValue(String value) { for (int i = 0; i < array.length(); i++) { JSONObject message = array.getJSONObject(i); String callbackId = message.getString(MESSAGE_CALLBACK_ID); - JSONObject response = new JSONObject(); - JSONObject responseData = handleMessage(message.getString(MESSAGE_DATA)); - if (callbackId != null) { - response.put(MESSAGE_RESPONSE_ID, callbackId); - response.put(MESSAGE_DATA, responseData); - String script = MESSAGE_JS_RESPONSE_SCRIPT.replace( - MESSAGE_PARAM_HOLDER, escape(response.toString())); - mWebView.evaluateJavascript(script, null); + 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) { @@ -76,7 +90,18 @@ public void onReceiveValue(String value) { }); } - protected abstract JSONObject handleMessage(String message); + + 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<String> getSupportedCmdList(); + + protected abstract void handleMessage(String message, MessageFinishCallback callback); @Nullable public static String unescape(@Nullable String value) { @@ -103,4 +128,20 @@ public static String escape(@Nullable String value) { 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); + } + } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/webview/QMUIWebViewClient.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/webview/QMUIWebViewClient.java index 938b7ba26..eb92eca73 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/webview/QMUIWebViewClient.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/webview/QMUIWebViewClient.java @@ -17,16 +17,16 @@ package com.qmuiteam.qmui.widget.webview; import android.graphics.Bitmap; -import android.os.Build; import android.os.SystemClock; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; 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 @@ -117,30 +117,15 @@ private void dispatchFullscreenRequestEvent(WebView webView) { } private void runJsCode(WebView webView, @NonNull String jsCode, @Nullable final Runnable finishAction) { - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) { - if (finishAction == null) { - webView.evaluateJavascript(jsCode, null); - } else { - webView.evaluateJavascript(jsCode, new ValueCallback<String>() { - @Override - public void onReceiveValue(String value) { - finishAction.run(); - } - }); - } - + if (finishAction == null) { + webView.evaluateJavascript(jsCode, null); } else { - // Usually, there is no chance to come here. - webView.loadUrl("javascript:" + jsCode); - if (finishAction != null) { - webView.postDelayed(new Runnable() { - @Override - public void run() { - finishAction.run(); - } - }, 250); - } - + webView.evaluateJavascript(jsCode, new ValueCallback<String>() { + @Override + public void onReceiveValue(String value) { + finishAction.run(); + } + }); } } } diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/webview/QMUIWebViewContainer.java b/qmui/src/main/java/com/qmuiteam/qmui/widget/webview/QMUIWebViewContainer.java index d44991a4e..4c401dd54 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/webview/QMUIWebViewContainer.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/widget/webview/QMUIWebViewContainer.java @@ -16,23 +16,19 @@ package com.qmuiteam.qmui.widget.webview; -import android.annotation.TargetApi; import android.content.Context; -import android.content.res.Configuration; -import android.graphics.Rect; import android.util.AttributeSet; import android.view.ViewGroup; -import android.view.WindowInsets; import android.webkit.WebView; import android.widget.FrameLayout; -import com.qmuiteam.qmui.util.QMUINotchHelper; -import com.qmuiteam.qmui.widget.QMUIWindowInsetLayout; - import androidx.annotation.NonNull; import androidx.core.view.WindowInsetsCompat; -public class QMUIWebViewContainer extends QMUIWindowInsetLayout { +import com.qmuiteam.qmui.layout.QMUIFrameLayout; +import com.qmuiteam.qmui.util.QMUIWindowInsetHelper; + +public class QMUIWebViewContainer extends QMUIFrameLayout { private QMUIWebView mWebView; private QMUIWebView.OnScrollChangeListener mOnScrollChangeListener; @@ -58,6 +54,7 @@ public void onScrollChange(WebView webView, int scrollX, int scrollY, int oldScr } }); addView(mWebView, getWebViewLayoutParams()); + QMUIWindowInsetHelper.handleWindowInsets(this, WindowInsetsCompat.Type.statusBars() | WindowInsetsCompat.Type.displayCutout()); } protected FrameLayout.LayoutParams getWebViewLayoutParams() { @@ -84,50 +81,4 @@ public void destroy() { public void setCustomOnScrollChangeListener(QMUIWebView.OnScrollChangeListener onScrollChangeListener) { mOnScrollChangeListener = onScrollChangeListener; } - - @Override - @TargetApi(19) - public boolean applySystemWindowInsets19(Rect insets) { - if (getFitsSystemWindows()) { - Rect childInsets = new Rect(insets); - mQMUIWindowInsetHelper.computeInsets(this, childInsets); - setPadding(childInsets.left, childInsets.top, childInsets.right, childInsets.bottom); - return true; - } - return super.applySystemWindowInsets19(insets); - } - - @Override - @TargetApi(21) - public boolean applySystemWindowInsets21(Object insets) { - if (getFitsSystemWindows()) { - int insetLeft = 0, insetRight = 0, insetTop = 0, insetBottom = 0; - if (insets instanceof WindowInsetsCompat) { - WindowInsetsCompat windowInsetsCompat = (WindowInsetsCompat) insets; - insetLeft = windowInsetsCompat.getSystemWindowInsetLeft(); - insetRight = windowInsetsCompat.getSystemWindowInsetRight(); - insetTop = windowInsetsCompat.getSystemWindowInsetTop(); - insetBottom = windowInsetsCompat.getSystemWindowInsetBottom(); - } else if (insets instanceof WindowInsets) { - WindowInsets windowInsets = (WindowInsets) insets; - insetLeft = windowInsets.getSystemWindowInsetLeft(); - insetRight = windowInsets.getSystemWindowInsetRight(); - insetTop = windowInsets.getSystemWindowInsetTop(); - insetBottom = windowInsets.getSystemWindowInsetBottom(); - } - - if (QMUINotchHelper.needFixLandscapeNotchAreaFitSystemWindow(this) && - getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { - insetLeft = Math.max(insetLeft, QMUINotchHelper.getSafeInsetLeft(this)); - insetRight = Math.max(insetRight, QMUINotchHelper.getSafeInsetRight(this)); - } - - Rect childInsets = new Rect(insetLeft, insetTop, insetRight, insetBottom); - mQMUIWindowInsetHelper.computeInsets(this, childInsets); - setPadding(childInsets.left, childInsets.top, childInsets.right, childInsets.bottom); - return true; - } - - return super.applySystemWindowInsets21(insets); - } } diff --git a/qmui/src/main/res/layout/qmui_bottom_sheet_dialog.xml b/qmui/src/main/res/layout/qmui_bottom_sheet_dialog.xml index d76d5b6f2..0c2d638a0 100644 --- a/qmui/src/main/res/layout/qmui_bottom_sheet_dialog.xml +++ b/qmui/src/main/res/layout/qmui_bottom_sheet_dialog.xml @@ -19,8 +19,7 @@ xmlns:tools="http://schemas.android.com/tools" android:id="@+id/container" android:layout_width="match_parent" - android:layout_height="match_parent" - android:fitsSystemWindows="true"> + android:layout_height="match_parent"> <androidx.coordinatorlayout.widget.CoordinatorLayout android:id="@+id/coordinator" diff --git a/qmui/src/main/res/layout/qmui_common_list_item.xml b/qmui/src/main/res/layout/qmui_common_list_item.xml index 15a8ecfbc..65c1c4aca 100644 --- a/qmui/src/main/res/layout/qmui_common_list_item.xml +++ b/qmui/src/main/res/layout/qmui_common_list_item.xml @@ -41,15 +41,6 @@ app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> - <androidx.constraintlayout.widget.Placeholder - android:id="@+id/group_list_item_holder_before_accessory" - android:layout_width="1px" - android:layout_height="1px" - android:layout_marginRight="?attr/qmui_common_list_item_accessory_margin_left" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintRight_toLeftOf="@+id/group_list_item_accessoryView" - app:layout_constraintTop_toTopOf="parent" - app:layout_goneMarginRight="0dp" /> <TextView android:id="@+id/group_list_item_textView" @@ -59,13 +50,14 @@ android:layout_marginRight="?attr/qmui_common_list_item_accessory_margin_left" android:ellipsize="end" android:includeFontPadding="false" + app:layout_constraintHorizontal_chainStyle="spread_inside" android:lineSpacingExtra="?attr/qmui_common_list_item_title_line_space" android:textColor="?attr/qmui_skin_support_common_list_title_color" android:textSize="?attr/qmui_common_list_item_title_h_text_size" app:layout_constrainedWidth="true" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toRightOf="@+id/group_list_item_imageView" - app:layout_constraintRight_toLeftOf="@+id/group_list_item_holder_before_accessory" + app:layout_constraintRight_toLeftOf="@+id/group_list_item_accessoryView" app:layout_constraintTop_toTopOf="parent" app:layout_goneMarginLeft="0dp" app:layout_goneMarginRight="0dp" @@ -88,22 +80,13 @@ app:layout_constraintHorizontal_bias="1" app:layout_constraintWidth_default="wrap" app:layout_constraintLeft_toRightOf="@+id/group_list_item_textView" - app:layout_constraintRight_toLeftOf="@+id/group_list_item_holder_before_accessory" + app:layout_constraintRight_toLeftOf="@+id/group_list_item_accessoryView" app:layout_constraintTop_toTopOf="parent" app:layout_goneMarginRight="0dp" app:qmui_skin_text_color="?attr/qmui_skin_support_common_list_detail_color"/> - <androidx.constraintlayout.widget.Placeholder - android:id="@+id/group_list_item_holder_after_title" - android:layout_width="1px" - android:layout_height="1px" - android:layout_marginLeft="?attr/qmui_common_list_item_holder_margin_with_title" - app:layout_constraintBottom_toBottomOf="@+id/group_list_item_textView" - app:layout_constraintLeft_toRightOf="@+id/group_list_item_textView" - app:layout_constraintTop_toTopOf="@+id/group_list_item_textView" /> - <androidx.appcompat.widget.AppCompatImageView android:id="@+id/group_list_item_tips_new" android:contentDescription="Update tips" diff --git a/qmui/src/main/res/values/qmui_attrs.xml b/qmui/src/main/res/values/qmui_attrs.xml index 50d201ea6..484635a97 100755 --- a/qmui/src/main/res/values/qmui_attrs.xml +++ b/qmui/src/main/res/values/qmui_attrs.xml @@ -27,49 +27,6 @@ </attr> <attr name="qmui_title" format="string"/> - <declare-styleable name="QMUILayout"> - <attr name="android:maxWidth"/> - <attr name="android:maxHeight"/> - <attr name="android:minWidth"/> - <attr name="android:minHeight"/> - <attr name="qmui_bottomDividerHeight" format="dimension"/> - <attr name="qmui_bottomDividerColor" format="color|reference"/> - <attr name="qmui_bottomDividerInsetLeft" format="dimension"/> - <attr name="qmui_bottomDividerInsetRight" format="dimension"/> - <attr name="qmui_topDividerHeight" format="dimension"/> - <attr name="qmui_topDividerColor" format="color|reference"/> - <attr name="qmui_topDividerInsetLeft" format="dimension"/> - <attr name="qmui_topDividerInsetRight" format="dimension"/> - <attr name="qmui_leftDividerWidth" format="dimension"/> - <attr name="qmui_leftDividerColor" format="color|reference"/> - <attr name="qmui_leftDividerInsetTop" format="dimension"/> - <attr name="qmui_leftDividerInsetBottom" format="dimension"/> - <attr name="qmui_rightDividerWidth" format="dimension"/> - <attr name="qmui_rightDividerColor" format="color|reference"/> - <attr name="qmui_rightDividerInsetTop" format="dimension"/> - <attr name="qmui_rightDividerInsetBottom" format="dimension"/> - <attr name="qmui_radius"/> - <attr name="qmui_borderColor"/> - <attr name="qmui_borderWidth"/> - <attr name="qmui_outerNormalColor" format="color|reference"/> - <attr name="qmui_hideRadiusSide" format="enum"> - <enum name="none" value="0"/> - <enum name="top" value="1"/> - <enum name="right" value="2"/> - <enum name="bottom" value="3"/> - <enum name="left" value="4"/> - </attr> - <attr name="qmui_showBorderOnlyBeforeL" format="boolean"/> - <attr name="qmui_shadowElevation" format="dimension"/> - <attr name="qmui_useThemeGeneralShadowElevation" format="boolean"/> - <attr name="qmui_shadowAlpha" format="float"/> - <attr name="qmui_outlineInsetTop" format="dimension"/> - <attr name="qmui_outlineInsetLeft" format="dimension"/> - <attr name="qmui_outlineInsetRight" format="dimension"/> - <attr name="qmui_outlineInsetBottom" format="dimension"/> - <attr name="qmui_outlineExcludePadding" format="boolean"/> - </declare-styleable> - <!--**************** QMUITabSegment ******************--> <declare-styleable name="QMUITabSegment"> <attr name="qmui_tab_indicator_height" format="dimension"/> @@ -126,15 +83,20 @@ <!--************ QMUITopBar ***********--> <declare-styleable name="QMUITopBar"> + <attr name="android:ellipsize"/> <attr name="qmui_topbar_title_gravity" format="enum"> <enum name="left_center" value="19"/> <enum name="center" value="17"/> </attr> <attr name="qmui_topbar_left_back_drawable_id" format="reference"/> + <attr name="qmui_topbar_left_back_width" format="dimension"/> + <attr name="qmui_topbar_clear_left_padding_when_add_left_back_view" format="boolean"/> <attr name="qmui_topbar_title_text_size" format="dimension"/> - <attr name="qmui_topbar_title_text_size_with_subtitle" format="dimension"/> <attr name="qmui_topbar_subtitle_text_size" format="dimension"/> <attr name="qmui_topbar_title_color" format="color"/> + <attr name="qmui_topbar_title_bold" format="boolean"/> + <attr name="qmui_topbar_title_text_size_with_subtitle" format="dimension"/> + <attr name="qmui_topbar_subtitle_bold" format="boolean"/> <attr name="qmui_topbar_subtitle_color" format="color"/> <attr name="qmui_topbar_title_margin_horizontal_when_no_btn_aside" format="dimension"/> <attr name="qmui_topbar_title_container_padding_horizontal" format="dimension"/> @@ -143,6 +105,7 @@ <attr name="qmui_topbar_text_btn_padding_horizontal" format="dimension"/> <attr name="qmui_topbar_text_btn_color_state_list" format="reference"/> <attr name="qmui_topbar_text_btn_text_size" format="dimension"/> + <attr name="qmui_topbar_text_btn_bold" format="boolean"/> </declare-styleable> <attr name="QMUITopBarStyle" format="reference"/> @@ -242,7 +205,9 @@ <declare-styleable name="QMUIProgressBar"> <attr name="qmui_type" format="enum"> <enum name="type_rect" value="0"/> - <enum name="type_circle" value="1"/> + <enum name="type_round_rect" value="1"/> + <enum name="type_circle" value="2"/> + <enum name="type_fill_circle" value="3"/> </attr> <attr name="qmui_progress_color" format="color"/> <attr name="qmui_background_color" format="color"/> @@ -269,73 +234,6 @@ <attr name="qmui_linkTextColor" format="color"/> </declare-styleable> - <!-- RoundWidget start --> - - <!-- 圆角是否要自适应为 View 高度的一半 --> - <attr name="qmui_isRadiusAdjustBounds" format="boolean"/> - <!-- 同时指定四个方向的圆角大小 --> - <attr name="qmui_radius" format="dimension"/> - <!-- 指定左上方圆角的大小 --> - <attr name="qmui_radiusTopLeft" format="dimension"/> - <!-- 指定右上方圆角的大小 --> - <attr name="qmui_radiusTopRight" format="dimension"/> - <!-- 指定左下方圆角的大小 --> - <attr name="qmui_radiusBottomLeft" format="dimension"/> - <!-- 指定右下方圆角的大小 --> - <attr name="qmui_radiusBottomRight" format="dimension"/> - - <attr name="QMUIButtonStyle" format="reference"/> - - <declare-styleable name="QMUIRoundButton"> - <attr name="qmui_backgroundColor"/> - <attr name="qmui_borderColor"/> - <attr name="qmui_borderWidth"/> - <attr name="qmui_isRadiusAdjustBounds"/> - <attr name="qmui_radius"/> - <attr name="qmui_radiusTopLeft"/> - <attr name="qmui_radiusTopRight"/> - <attr name="qmui_radiusBottomLeft"/> - <attr name="qmui_radiusBottomRight"/> - </declare-styleable> - - <declare-styleable name="QMUIRoundFrameLayout"> - <attr name="qmui_backgroundColor"/> - <attr name="qmui_borderColor"/> - <attr name="qmui_borderWidth"/> - <attr name="qmui_isRadiusAdjustBounds"/> - <attr name="qmui_radius"/> - <attr name="qmui_radiusTopLeft"/> - <attr name="qmui_radiusTopRight"/> - <attr name="qmui_radiusBottomLeft"/> - <attr name="qmui_radiusBottomRight"/> - </declare-styleable> - - <declare-styleable name="QMUIRoundLinearLayout"> - <attr name="qmui_backgroundColor"/> - <attr name="qmui_borderColor"/> - <attr name="qmui_borderWidth"/> - <attr name="qmui_isRadiusAdjustBounds"/> - <attr name="qmui_radius"/> - <attr name="qmui_radiusTopLeft"/> - <attr name="qmui_radiusTopRight"/> - <attr name="qmui_radiusBottomLeft"/> - <attr name="qmui_radiusBottomRight"/> - </declare-styleable> - - <declare-styleable name="QMUIRoundRelativeLayout"> - <attr name="qmui_backgroundColor"/> - <attr name="qmui_borderColor"/> - <attr name="qmui_borderWidth"/> - <attr name="qmui_isRadiusAdjustBounds"/> - <attr name="qmui_radius"/> - <attr name="qmui_radiusTopLeft"/> - <attr name="qmui_radiusTopRight"/> - <attr name="qmui_radiusBottomLeft"/> - <attr name="qmui_radiusBottomRight"/> - </declare-styleable> - - <!-- RoundWidget end --> - <!-- QMUICollapsingTopBarLayout start --> <declare-styleable name="QMUICollapsingTopBarLayout"> <attr name="qmui_expandedTitleMargin" format="dimension"/> diff --git a/qmui/src/main/res/values/qmui_attrs_alpha.xml b/qmui/src/main/res/values/qmui_attrs_alpha.xml new file mode 100644 index 000000000..9c702f307 --- /dev/null +++ b/qmui/src/main/res/values/qmui_attrs_alpha.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <declare-styleable name="QMUIAlphaButton"> + <attr name="qmui_alpha_pressed" /> + <attr name="qmui_alpha_disabled" /> + </declare-styleable> + + <declare-styleable name="QMUIAlphaConstraintLayout"> + <attr name="qmui_alpha_pressed" /> + <attr name="qmui_alpha_disabled" /> + </declare-styleable> + + <declare-styleable name="QMUIAlphaFrameLayout"> + <attr name="qmui_alpha_pressed" /> + <attr name="qmui_alpha_disabled" /> + </declare-styleable> + + <declare-styleable name="QMUIAlphaImageButton"> + <attr name="qmui_alpha_pressed" /> + <attr name="qmui_alpha_disabled" /> + </declare-styleable> + + <declare-styleable name="QMUIAlphaLinearLayout"> + <attr name="qmui_alpha_pressed" /> + <attr name="qmui_alpha_disabled" /> + </declare-styleable> + + <declare-styleable name="QMUIAlphaRelativeLayout"> + <attr name="qmui_alpha_pressed" /> + <attr name="qmui_alpha_disabled" /> + </declare-styleable> + + <declare-styleable name="QMUIAlphaTextView"> + <attr name="qmui_alpha_pressed" /> + <attr name="qmui_alpha_disabled" /> + </declare-styleable> +</resources> \ No newline at end of file diff --git a/qmui/src/main/res/values/qmui_attrs_base.xml b/qmui/src/main/res/values/qmui_attrs_base.xml index 93ef364a5..28144031d 100644 --- a/qmui/src/main/res/values/qmui_attrs_base.xml +++ b/qmui/src/main/res/values/qmui_attrs_base.xml @@ -64,7 +64,6 @@ <attr name="qmui_common_list_item_detail_h_text_size" format="dimension" /> <attr name="qmui_common_list_item_detail_line_space" format="dimension" /> <attr name="qmui_common_list_item_detail_h_margin_with_title" format="dimension" /> - <attr name="qmui_common_list_item_detail_h_margin_with_title_large" format="dimension" /> <attr name="qmui_common_list_item_detail_v_margin_with_title" format="dimension" /> <attr name="qmui_common_list_item_holder_margin_with_title" format="dimension"/> @@ -287,6 +286,7 @@ <attr name="qmui_bottom_sheet_background_dim_amount" format="float" /> <attr name="qmui_bottom_sheet_cancel_btn_height" format="dimension"/> <attr name="qmui_bottom_sheet_list_item_height" format="dimension"/> + <attr name="qmui_bottom_sheet_list_item_separator_height" format="dimension"/> <attr name="qmui_bottom_sheet_list_item_icon_size" format="dimension" /> <attr name="qmui_bottom_sheet_list_item_icon_margin_right" format="dimension" /> <attr name="qmui_bottom_sheet_list_item_red_point_size" format="dimension"/> diff --git a/qmui/src/main/res/values/qmui_attrs_layout.xml b/qmui/src/main/res/values/qmui_attrs_layout.xml new file mode 100644 index 000000000..caa9aa6f1 --- /dev/null +++ b/qmui/src/main/res/values/qmui_attrs_layout.xml @@ -0,0 +1,243 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <declare-styleable name="QMUILayout"> + <attr name="android:maxWidth" /> + <attr name="android:maxHeight" /> + <attr name="android:minWidth" /> + <attr name="android:minHeight" /> + <attr name="qmui_bottomDividerHeight" format="dimension" /> + <attr name="qmui_bottomDividerColor" format="color|reference" /> + <attr name="qmui_bottomDividerInsetLeft" format="dimension" /> + <attr name="qmui_bottomDividerInsetRight" format="dimension" /> + <attr name="qmui_topDividerHeight" format="dimension" /> + <attr name="qmui_topDividerColor" format="color|reference" /> + <attr name="qmui_topDividerInsetLeft" format="dimension" /> + <attr name="qmui_topDividerInsetRight" format="dimension" /> + <attr name="qmui_leftDividerWidth" format="dimension" /> + <attr name="qmui_leftDividerColor" format="color|reference" /> + <attr name="qmui_leftDividerInsetTop" format="dimension" /> + <attr name="qmui_leftDividerInsetBottom" format="dimension" /> + <attr name="qmui_rightDividerWidth" format="dimension" /> + <attr name="qmui_rightDividerColor" format="color|reference" /> + <attr name="qmui_rightDividerInsetTop" format="dimension" /> + <attr name="qmui_rightDividerInsetBottom" format="dimension" /> + <attr name="qmui_radius" /> + <attr name="qmui_borderColor" /> + <attr name="qmui_borderWidth" /> + <attr name="qmui_outerNormalColor" format="color|reference" /> + <attr name="qmui_hideRadiusSide" format="enum"> + <enum name="none" value="0" /> + <enum name="top" value="1" /> + <enum name="right" value="2" /> + <enum name="bottom" value="3" /> + <enum name="left" value="4" /> + </attr> + <attr name="qmui_showBorderOnlyBeforeL" format="boolean" /> + <attr name="qmui_shadowElevation" format="dimension" /> + <attr name="qmui_useThemeGeneralShadowElevation" format="boolean" /> + <attr name="qmui_shadowAlpha" format="float" /> + <attr name="qmui_outlineInsetTop" format="dimension" /> + <attr name="qmui_outlineInsetLeft" format="dimension" /> + <attr name="qmui_outlineInsetRight" format="dimension" /> + <attr name="qmui_outlineInsetBottom" format="dimension" /> + <attr name="qmui_outlineExcludePadding" format="boolean" /> + </declare-styleable> + + <declare-styleable name="QMUIButton"> + <attr name="qmui_bottomDividerHeight" /> + <attr name="qmui_bottomDividerColor" /> + <attr name="qmui_bottomDividerInsetLeft" /> + <attr name="qmui_bottomDividerInsetRight" /> + <attr name="qmui_topDividerHeight" /> + <attr name="qmui_topDividerColor" /> + <attr name="qmui_topDividerInsetLeft" /> + <attr name="qmui_topDividerInsetRight" /> + <attr name="qmui_leftDividerWidth" /> + <attr name="qmui_leftDividerColor" /> + <attr name="qmui_leftDividerInsetTop" /> + <attr name="qmui_leftDividerInsetBottom" /> + <attr name="qmui_rightDividerWidth" /> + <attr name="qmui_rightDividerColor" /> + <attr name="qmui_rightDividerInsetTop" /> + <attr name="qmui_rightDividerInsetBottom" /> + <attr name="qmui_radius" /> + <attr name="qmui_borderColor" /> + <attr name="qmui_borderWidth" /> + <attr name="qmui_outerNormalColor" /> + <attr name="qmui_hideRadiusSide" /> + <attr name="qmui_showBorderOnlyBeforeL" /> + <attr name="qmui_shadowElevation" /> + <attr name="qmui_useThemeGeneralShadowElevation" /> + <attr name="qmui_shadowAlpha" /> + <attr name="qmui_outlineInsetTop" /> + <attr name="qmui_outlineInsetLeft" /> + <attr name="qmui_outlineInsetRight" /> + <attr name="qmui_outlineInsetBottom" /> + <attr name="qmui_outlineExcludePadding" /> + </declare-styleable> + + <declare-styleable name="QMUIConstraintLayout"> + <attr name="qmui_bottomDividerHeight" /> + <attr name="qmui_bottomDividerColor" /> + <attr name="qmui_bottomDividerInsetLeft" /> + <attr name="qmui_bottomDividerInsetRight" /> + <attr name="qmui_topDividerHeight" /> + <attr name="qmui_topDividerColor" /> + <attr name="qmui_topDividerInsetLeft" /> + <attr name="qmui_topDividerInsetRight" /> + <attr name="qmui_leftDividerWidth" /> + <attr name="qmui_leftDividerColor" /> + <attr name="qmui_leftDividerInsetTop" /> + <attr name="qmui_leftDividerInsetBottom" /> + <attr name="qmui_rightDividerWidth" /> + <attr name="qmui_rightDividerColor" /> + <attr name="qmui_rightDividerInsetTop" /> + <attr name="qmui_rightDividerInsetBottom" /> + <attr name="qmui_radius" /> + <attr name="qmui_borderColor" /> + <attr name="qmui_borderWidth" /> + <attr name="qmui_outerNormalColor" /> + <attr name="qmui_hideRadiusSide" /> + <attr name="qmui_showBorderOnlyBeforeL" /> + <attr name="qmui_shadowElevation" /> + <attr name="qmui_useThemeGeneralShadowElevation" /> + <attr name="qmui_shadowAlpha" /> + <attr name="qmui_outlineInsetTop" /> + <attr name="qmui_outlineInsetLeft" /> + <attr name="qmui_outlineInsetRight" /> + <attr name="qmui_outlineInsetBottom" /> + <attr name="qmui_outlineExcludePadding" /> + </declare-styleable> + + <declare-styleable name="QMUIFrameLayout"> + <attr name="qmui_bottomDividerHeight" /> + <attr name="qmui_bottomDividerColor" /> + <attr name="qmui_bottomDividerInsetLeft" /> + <attr name="qmui_bottomDividerInsetRight" /> + <attr name="qmui_topDividerHeight" /> + <attr name="qmui_topDividerColor" /> + <attr name="qmui_topDividerInsetLeft" /> + <attr name="qmui_topDividerInsetRight" /> + <attr name="qmui_leftDividerWidth" /> + <attr name="qmui_leftDividerColor" /> + <attr name="qmui_leftDividerInsetTop" /> + <attr name="qmui_leftDividerInsetBottom" /> + <attr name="qmui_rightDividerWidth" /> + <attr name="qmui_rightDividerColor" /> + <attr name="qmui_rightDividerInsetTop" /> + <attr name="qmui_rightDividerInsetBottom" /> + <attr name="qmui_radius" /> + <attr name="qmui_borderColor" /> + <attr name="qmui_borderWidth" /> + <attr name="qmui_outerNormalColor" /> + <attr name="qmui_hideRadiusSide" /> + <attr name="qmui_showBorderOnlyBeforeL" /> + <attr name="qmui_shadowElevation" /> + <attr name="qmui_useThemeGeneralShadowElevation" /> + <attr name="qmui_shadowAlpha" /> + <attr name="qmui_outlineInsetTop" /> + <attr name="qmui_outlineInsetLeft" /> + <attr name="qmui_outlineInsetRight" /> + <attr name="qmui_outlineInsetBottom" /> + <attr name="qmui_outlineExcludePadding" /> + </declare-styleable> + + <declare-styleable name="QMUILinearLayout"> + <attr name="qmui_bottomDividerHeight" /> + <attr name="qmui_bottomDividerColor" /> + <attr name="qmui_bottomDividerInsetLeft" /> + <attr name="qmui_bottomDividerInsetRight" /> + <attr name="qmui_topDividerHeight" /> + <attr name="qmui_topDividerColor" /> + <attr name="qmui_topDividerInsetLeft" /> + <attr name="qmui_topDividerInsetRight" /> + <attr name="qmui_leftDividerWidth" /> + <attr name="qmui_leftDividerColor" /> + <attr name="qmui_leftDividerInsetTop" /> + <attr name="qmui_leftDividerInsetBottom" /> + <attr name="qmui_rightDividerWidth" /> + <attr name="qmui_rightDividerColor" /> + <attr name="qmui_rightDividerInsetTop" /> + <attr name="qmui_rightDividerInsetBottom" /> + <attr name="qmui_radius" /> + <attr name="qmui_borderColor" /> + <attr name="qmui_borderWidth" /> + <attr name="qmui_outerNormalColor" /> + <attr name="qmui_hideRadiusSide" /> + <attr name="qmui_showBorderOnlyBeforeL" /> + <attr name="qmui_shadowElevation" /> + <attr name="qmui_useThemeGeneralShadowElevation" /> + <attr name="qmui_shadowAlpha" /> + <attr name="qmui_outlineInsetTop" /> + <attr name="qmui_outlineInsetLeft" /> + <attr name="qmui_outlineInsetRight" /> + <attr name="qmui_outlineInsetBottom" /> + <attr name="qmui_outlineExcludePadding" /> + </declare-styleable> + + <declare-styleable name="QMUIPriorityLinearLayout"> + <attr name="qmui_bottomDividerHeight" /> + <attr name="qmui_bottomDividerColor" /> + <attr name="qmui_bottomDividerInsetLeft" /> + <attr name="qmui_bottomDividerInsetRight" /> + <attr name="qmui_topDividerHeight" /> + <attr name="qmui_topDividerColor" /> + <attr name="qmui_topDividerInsetLeft" /> + <attr name="qmui_topDividerInsetRight" /> + <attr name="qmui_leftDividerWidth" /> + <attr name="qmui_leftDividerColor" /> + <attr name="qmui_leftDividerInsetTop" /> + <attr name="qmui_leftDividerInsetBottom" /> + <attr name="qmui_rightDividerWidth" /> + <attr name="qmui_rightDividerColor" /> + <attr name="qmui_rightDividerInsetTop" /> + <attr name="qmui_rightDividerInsetBottom" /> + <attr name="qmui_radius" /> + <attr name="qmui_borderColor" /> + <attr name="qmui_borderWidth" /> + <attr name="qmui_outerNormalColor" /> + <attr name="qmui_hideRadiusSide" /> + <attr name="qmui_showBorderOnlyBeforeL" /> + <attr name="qmui_shadowElevation" /> + <attr name="qmui_useThemeGeneralShadowElevation" /> + <attr name="qmui_shadowAlpha" /> + <attr name="qmui_outlineInsetTop" /> + <attr name="qmui_outlineInsetLeft" /> + <attr name="qmui_outlineInsetRight" /> + <attr name="qmui_outlineInsetBottom" /> + <attr name="qmui_outlineExcludePadding" /> + </declare-styleable> + + <declare-styleable name="QMUIRelativeLayout"> + <attr name="qmui_bottomDividerHeight" /> + <attr name="qmui_bottomDividerColor" /> + <attr name="qmui_bottomDividerInsetLeft" /> + <attr name="qmui_bottomDividerInsetRight" /> + <attr name="qmui_topDividerHeight" /> + <attr name="qmui_topDividerColor" /> + <attr name="qmui_topDividerInsetLeft" /> + <attr name="qmui_topDividerInsetRight" /> + <attr name="qmui_leftDividerWidth" /> + <attr name="qmui_leftDividerColor" /> + <attr name="qmui_leftDividerInsetTop" /> + <attr name="qmui_leftDividerInsetBottom" /> + <attr name="qmui_rightDividerWidth" /> + <attr name="qmui_rightDividerColor" /> + <attr name="qmui_rightDividerInsetTop" /> + <attr name="qmui_rightDividerInsetBottom" /> + <attr name="qmui_radius" /> + <attr name="qmui_borderColor" /> + <attr name="qmui_borderWidth" /> + <attr name="qmui_outerNormalColor" /> + <attr name="qmui_hideRadiusSide" /> + <attr name="qmui_showBorderOnlyBeforeL" /> + <attr name="qmui_shadowElevation" /> + <attr name="qmui_useThemeGeneralShadowElevation" /> + <attr name="qmui_shadowAlpha" /> + <attr name="qmui_outlineInsetTop" /> + <attr name="qmui_outlineInsetLeft" /> + <attr name="qmui_outlineInsetRight" /> + <attr name="qmui_outlineInsetBottom" /> + <attr name="qmui_outlineExcludePadding" /> + </declare-styleable> +</resources> \ No newline at end of file diff --git a/qmui/src/main/res/values/qmui_attrs_round.xml b/qmui/src/main/res/values/qmui_attrs_round.xml new file mode 100644 index 000000000..5a0ffa49b --- /dev/null +++ b/qmui/src/main/res/values/qmui_attrs_round.xml @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- RoundWidget start --> + + <!-- 圆角是否要自适应为 View 高度的一半 --> + <attr name="qmui_isRadiusAdjustBounds" format="boolean"/> + <!-- 同时指定四个方向的圆角大小 --> + <attr name="qmui_radius" format="dimension"/> + <!-- 指定左上方圆角的大小 --> + <attr name="qmui_radiusTopLeft" format="dimension"/> + <!-- 指定右上方圆角的大小 --> + <attr name="qmui_radiusTopRight" format="dimension"/> + <!-- 指定左下方圆角的大小 --> + <attr name="qmui_radiusBottomLeft" format="dimension"/> + <!-- 指定右下方圆角的大小 --> + <attr name="qmui_radiusBottomRight" format="dimension"/> + + <attr name="QMUIButtonStyle" format="reference"/> + + <declare-styleable name="QMUIRoundButton"> + <attr name="qmui_backgroundColor"/> + <attr name="qmui_borderColor"/> + <attr name="qmui_borderWidth"/> + <attr name="qmui_isRadiusAdjustBounds"/> + <attr name="qmui_radius"/> + <attr name="qmui_radiusTopLeft"/> + <attr name="qmui_radiusTopRight"/> + <attr name="qmui_radiusBottomLeft"/> + <attr name="qmui_radiusBottomRight"/> + </declare-styleable> + + <declare-styleable name="QMUIRoundFrameLayout"> + <attr name="qmui_backgroundColor"/> + <attr name="qmui_borderColor"/> + <attr name="qmui_borderWidth"/> + <attr name="qmui_isRadiusAdjustBounds"/> + <attr name="qmui_radius"/> + <attr name="qmui_radiusTopLeft"/> + <attr name="qmui_radiusTopRight"/> + <attr name="qmui_radiusBottomLeft"/> + <attr name="qmui_radiusBottomRight"/> + </declare-styleable> + + <declare-styleable name="QMUIRoundLinearLayout"> + <attr name="qmui_backgroundColor"/> + <attr name="qmui_borderColor"/> + <attr name="qmui_borderWidth"/> + <attr name="qmui_isRadiusAdjustBounds"/> + <attr name="qmui_radius"/> + <attr name="qmui_radiusTopLeft"/> + <attr name="qmui_radiusTopRight"/> + <attr name="qmui_radiusBottomLeft"/> + <attr name="qmui_radiusBottomRight"/> + </declare-styleable> + + <declare-styleable name="QMUIRoundRelativeLayout"> + <attr name="qmui_backgroundColor"/> + <attr name="qmui_borderColor"/> + <attr name="qmui_borderWidth"/> + <attr name="qmui_isRadiusAdjustBounds"/> + <attr name="qmui_radius"/> + <attr name="qmui_radiusTopLeft"/> + <attr name="qmui_radiusTopRight"/> + <attr name="qmui_radiusBottomLeft"/> + <attr name="qmui_radiusBottomRight"/> + </declare-styleable> + + <!-- RoundWidget end --> +</resources> \ No newline at end of file diff --git a/qmui/src/main/res/values/qmui_ids.xml b/qmui/src/main/res/values/qmui_ids.xml index ac12aa61f..d33f1d51c 100644 --- a/qmui/src/main/res/values/qmui_ids.xml +++ b/qmui/src/main/res/values/qmui_ids.xml @@ -17,6 +17,8 @@ <resources> <!-- dialog --> + <item name="qmui_dialog_root_layout" type="id"/> + <item name="qmui_dialog_layout" type="id"/> <item name="qmui_dialog_title_id" type="id"/> <item name="qmui_dialog_operator_layout_id" type="id"/> <item name="qmui_dialog_content_id" type="id"/> @@ -24,6 +26,8 @@ <item name="qmui_dialog_edit_right_icon" type="id"/> <item name="qmui_bottom_sheet_title" type="id"/> <item name="qmui_bottom_sheet_cancel" type="id"/> + + <item name="qmui_tip_content_id" type="id" /> <item name="qmui_tab_segment_item_id" type="id"/> @@ -33,19 +37,31 @@ <item name="qmui_view_offset_helper" type="id"/> - <item name="qmui_window_inset_keyboard_area_consumer" type="id"/> - - <item name="qmui_do_not_intercept_keyboard_inset" type="id"/> - <item name="qmui_popup_close_btn_id" type="id"/> <item name="qmui_skin_current" type="id"/> <item name="qmui_skin_value" type="id" /> <item name="qmui_skin_default_attr_provider" type="id" /> - <item name="qmui_skin_dispatch_interceptor" type="id" /> + <item name="qmui_skin_apply_listener" type="id" /> <item name="qmui_skin_skip_for_maker" type="id"/> <item name="qmui_skin_adapter" type="id"/> + <item name="qmui_skin_intercept_dispatch" type="id"/> + <item name="qmui_skin_ignore_apply" type="id"/> <item name="qmui_click_timestamp" type="id"/> <item name="qmui_click_debounce_action" type="id"/> + + <item name="qmui_exposure_register" type="id"/> + <item name="qmui_exposure_config" type="id"/> + <item name="qmui_exposure_data" type="id"/> + <item name="qmui_exposure_effect_list" type="id"/> + <item name="qmui_exposure_last_data" type="id"/> + <item name="qmui_exposure_ing" type="id"/> + <item name="qmui_exposure_holder" type="id"/> + <item name="qmui_exposure_debounce" type="id"/> + <item name="qmui_exposure_parent_expose_request" type="id"/> + <item name="qmui_exposure_recycler_collection" type="id"/> + <item name="qmui_exposure_custom_effect" type="id"/> + <item name="qmui_exposure_is_recycler_container" type="id"/> + <item name="qmui_exposure_custom_check_trigger" type="id" /> </resources> \ No newline at end of file diff --git a/qmui/src/main/res/values/qmui_style_widget.xml b/qmui/src/main/res/values/qmui_style_widget.xml index 97077d1ce..bc921f29f 100644 --- a/qmui/src/main/res/values/qmui_style_widget.xml +++ b/qmui/src/main/res/values/qmui_style_widget.xml @@ -28,7 +28,8 @@ <style name="QMUI.TabSegment.SignCount"> <item name="android:layout_width">wrap_content</item> <item name="android:layout_height">?attr/qmui_tab_sign_count_view_min_size</item> - <item name="qmui_backgroundColor">?attr/qmui_skin_support_tab_sign_count_view_bg_color</item> + <item name="qmui_backgroundColor">?attr/qmui_skin_support_tab_sign_count_view_bg_color + </item> <item name="qmui_isRadiusAdjustBounds">true</item> <item name="android:textSize">10sp</item> <item name="android:textColor">?attr/qmui_skin_support_tab_sign_count_view_text_color</item> @@ -101,19 +102,24 @@ <!--********************* TopBar *********************--> <style name="QMUI.TopBar"> <item name="android:background">?attr/qmui_skin_support_topbar_bg</item> + <item name="android:ellipsize">end</item> <item name="qmui_bottomDividerHeight">1px</item> <item name="qmui_bottomDividerColor">?attr/qmui_skin_support_topbar_separator_color</item> <item name="qmui_topbar_left_back_drawable_id">@drawable/qmui_icon_topbar_back</item> + <item name="qmui_topbar_clear_left_padding_when_add_left_back_view">false</item> + <item name="qmui_topbar_left_back_width">-1dp</item> <item name="qmui_topbar_title_gravity">center</item> <item name="android:paddingLeft">4dp</item> <item name="android:paddingRight">4dp</item> <item name="qmui_topbar_title_color">?attr/qmui_skin_support_topbar_title_color</item> <item name="qmui_topbar_title_text_size">17sp</item> + <item name="qmui_topbar_title_bold">false</item> <item name="qmui_topbar_title_text_size_with_subtitle">16sp</item> <item name="qmui_topbar_title_margin_horizontal_when_no_btn_aside">0dp</item> <item name="qmui_topbar_title_container_padding_horizontal">8dp</item> <item name="qmui_topbar_subtitle_text_size">11sp</item> <item name="qmui_topbar_subtitle_color">?attr/qmui_skin_support_topbar_subtitle_color</item> + <item name="qmui_topbar_subtitle_bold">false</item> <item name="qmui_topbar_image_btn_width">48dp</item> <item name="qmui_topbar_image_btn_height">48dp</item> <item name="qmui_topbar_text_btn_padding_horizontal">12dp</item> @@ -121,6 +127,7 @@ ?attr/qmui_skin_support_topbar_text_btn_color_state_list </item> <item name="qmui_topbar_text_btn_text_size">16sp</item> + <item name="qmui_topbar_text_btn_bold">false</item> </style> <style name="QMUI.CollapsingTopBarLayoutExpanded"> @@ -312,7 +319,7 @@ <style name="QMUI.TipNew"> <item name="android:layout_width">37dp</item> <item name="android:layout_height">17dp</item> - <item name="android:background">@drawable/qmui_icon_tip_new</item> + <item name="android:src">@drawable/qmui_icon_tip_new</item> </style> <style name="QMUI.Slider"> @@ -374,10 +381,18 @@ <item name="qmui_pull_load_more_arrow">@drawable/qmui_icon_pull_down</item> <item name="qmui_pull_load_more_pull_text">上拉加载更多</item> <item name="qmui_pull_load_more_release_text">松手加载更多</item> - <item name="qmui_skin_support_pull_load_more_bg_color">?attr/qmui_skin_support_pull_load_more_bg_color</item> - <item name="qmui_skin_support_pull_load_more_loading_tint_color">?attr/qmui_skin_support_pull_load_more_loading_tint_color</item> - <item name="qmui_skin_support_pull_load_more_arrow_tint_color">?attr/qmui_skin_support_pull_load_more_arrow_tint_color</item> - <item name="qmui_skin_support_pull_load_more_text_color">?attr/qmui_skin_support_pull_load_more_text_color</item> + <item name="qmui_skin_support_pull_load_more_bg_color"> + ?attr/qmui_skin_support_pull_load_more_bg_color + </item> + <item name="qmui_skin_support_pull_load_more_loading_tint_color"> + ?attr/qmui_skin_support_pull_load_more_loading_tint_color + </item> + <item name="qmui_skin_support_pull_load_more_arrow_tint_color"> + ?attr/qmui_skin_support_pull_load_more_arrow_tint_color + </item> + <item name="qmui_skin_support_pull_load_more_text_color"> + ?attr/qmui_skin_support_pull_load_more_text_color + </item> </style> <!-- QMUIRadiusImageView --> diff --git a/qmui/src/main/res/values/qmui_themes.xml b/qmui/src/main/res/values/qmui_themes.xml index 02194e145..deee61e0b 100644 --- a/qmui/src/main/res/values/qmui_themes.xml +++ b/qmui/src/main/res/values/qmui_themes.xml @@ -182,7 +182,6 @@ <item name="qmui_common_list_item_detail_h_text_size">14sp</item> <item name="qmui_common_list_item_detail_line_space">5dp</item> <item name="qmui_common_list_item_detail_h_margin_with_title">20dp</item> - <item name="qmui_common_list_item_detail_h_margin_with_title_large">52dp</item> <item name="qmui_common_list_item_detail_v_margin_with_title">4dp</item> <item name="qmui_common_list_item_holder_margin_with_title">8dp</item> <item name="qmui_common_list_item_chevron">@drawable/qmui_icon_chevron</item> @@ -335,6 +334,7 @@ <item name="qmui_bottom_sheet_background_dim_amount">0.6</item> <item name="qmui_bottom_sheet_cancel_btn_height">56dp</item> <item name="qmui_bottom_sheet_list_item_height">56dp</item> + <item name="qmui_bottom_sheet_list_item_separator_height">1px</item> <item name="qmui_bottom_sheet_list_item_icon_size">22dp</item> <item name="qmui_bottom_sheet_list_item_red_point_size">@dimen/qmui_tips_point_size</item> <item name="qmui_bottom_sheet_list_item_icon_margin_right">12dp</item> diff --git a/qmuidemo/build.gradle b/qmuidemo/build.gradle deleted file mode 100644 index 22b9e2d5b..000000000 --- a/qmuidemo/build.gradle +++ /dev/null @@ -1,106 +0,0 @@ -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' -apply plugin: 'kotlin-kapt' - - -def getVersion() { - def gitVersion - try { - def cmd = 'git rev-list HEAD --count' - gitVersion = cmd.execute().text.trim().toInteger() - } catch (Exception e) { - gitVersion = 1 - } - return gitVersion -} - -def gitVersion = getVersion() -def javaVersion = JavaVersion.VERSION_1_8 - -android { - signingConfigs { - Properties properties = new Properties() - File propFile = project.file('release.properties') - if (propFile.exists()) { - properties.load(propFile.newDataInputStream()) - } - release { - keyAlias properties.getProperty("RELEASE_KEY_ALIAS") - keyPassword properties.getProperty("RELEASE_KEY_PASSWORD") - storeFile file('qmuidemo.keystore') - storePassword properties.getProperty("RELEASE_STORE_PASSWORD") - v2SigningEnabled false - } - } - compileSdkVersion parent.ext.compileSdkVersion - - // for butterknife, see https://github.com/JakeWharton/butterknife/blob/master/CHANGELOG.md#version-900-rc2-2018-11-19 - compileOptions { - sourceCompatibility javaVersion - targetCompatibility javaVersion - } - - defaultConfig { - applicationId "com.qmuiteam.qmuidemo" - minSdkVersion parent.ext.minSdkVersion - targetSdkVersion parent.ext.targetSdkVersion - versionCode gitVersion - versionName QMUI_VERSION - } - buildTypes { - debug { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - release { - minifyEnabled true - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - signingConfig signingConfigs.release - } - } - // 避免 lint 检测出错时停止构建 - lintOptions { - abortOnError false - } -} - - -//apply plugin: 'com.qmuiteam.qmui.skinMaker' -//skinMaker{ -// file rootProject.file('qmuidemo-skin-code-generator-source') -//} - - - -// 加@aar与不加@aar的区别: -// http://stackoverflow.com/questions/30157575/why-should-i-include-a-gradle-dependency-as-aar -dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation "androidx.appcompat:appcompat:$appcompatVersion" - implementation "androidx.annotation:annotation:$annotationVersion" - implementation "com.google.android.material:material:$materialVersion" - implementation "com.jakewharton:butterknife:$butterknifeVersion" - kapt "com.jakewharton:butterknife-compiler:$butterknifeVersion" - implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" -// implementation 'com.qmuiteam:qmuilint:1.0.1' -// implementation 'com.qmuiteam:arch:0.3.1' -// implementation 'com.qmuiteam:qmui:1.2.0' - implementation project(':lib') - api project(':lint') - implementation project(':qmui') - implementation project(':arch') - kapt project(':compiler') - kapt project(':arch-compiler') - implementation project(":skin-maker") - - //leak - debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4' - releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4' - testImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4' - //test - testImplementation 'junit:junit:4.12' - // androidTestImplementation 'com.android.support.test:runner:1.0.2' - // androidTestImplementation 'com.android.support.test:rules:1.0.2' // Set this dependency to use JUnit 4 rules - // androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' // Set this dependency to build and run Espresso tests -} diff --git a/qmuidemo/build.gradle.kts b/qmuidemo/build.gradle.kts new file mode 100644 index 000000000..316f81b04 --- /dev/null +++ b/qmuidemo/build.gradle.kts @@ -0,0 +1,105 @@ +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") +} diff --git a/qmuidemo/proguard-rules.pro b/qmuidemo/proguard-rules.pro index 41979319b..26dc285c0 100644 --- a/qmuidemo/proguard-rules.pro +++ b/qmuidemo/proguard-rules.pro @@ -17,5 +17,13 @@ #} -keep class **_FragmentFinder { *; } --keep class com.qmuiteam.qmui.arch.record.** { *; } --keep class androidx.fragment.app.* { *; } \ No newline at end of file +-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.**{*;} \ No newline at end of file diff --git a/qmuidemo/src/main/AndroidManifest.xml b/qmuidemo/src/main/AndroidManifest.xml index a2deefd27..9b61f97a2 100644 --- a/qmuidemo/src/main/AndroidManifest.xml +++ b/qmuidemo/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.VIBRATE" /> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <application android:name=".QDApplication" @@ -35,10 +36,12 @@ android:name=".QDMainActivity" android:label="@string/app_name" android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|screenLayout|uiMode" - android:windowSoftInputMode="stateAlwaysHidden|adjustResize"/> + android:windowSoftInputMode="stateAlwaysHidden|adjustResize" + android:exported="false"/> <activity android:name=".activity.LauncherActivity" - android:theme="@style/AppTheme.Launcher"> + android:theme="@style/AppTheme.Launcher" + android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> @@ -48,12 +51,14 @@ <activity android:name=".activity.TranslucentActivity" android:label="@string/app_name" + android:exported="false" android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|screenLayout|uiMode" android:windowSoftInputMode="stateAlwaysHidden|adjustResize"> </activity> <activity android:name=".activity.ArchTestActivity" + android:exported="false" android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|screenLayout|uiMode" android:label="@string/app_name" android:windowSoftInputMode="stateAlwaysHidden|adjustResize"> @@ -61,11 +66,26 @@ <activity android:name=".activity.TestArchInViewPagerActivity" + android:exported="false" android:configChanges="orientation|keyboardHidden|screenSize" android:label="@string/app_name" android:windowSoftInputMode="stateAlwaysHidden|adjustResize"> </activity> + <activity + android:name="com.qmuiteam.photo.activity.QMUIPhotoViewerActivity" + android:screenOrientation="portrait" + tools:ignore="LockedOrientationActivity"> + + </activity> + + <activity + android:name="com.qmuiteam.photo.activity.QMUIPhotoPickerActivity" + android:screenOrientation="portrait" + tools:ignore="LockedOrientationActivity"> + + </activity> + </application> </manifest> diff --git a/qmuidemo/src/main/assets/demo.html b/qmuidemo/src/main/assets/demo.html index 446b9377d..c798e2fb7 100644 --- a/qmuidemo/src/main/assets/demo.html +++ b/qmuidemo/src/main/assets/demo.html @@ -11,10 +11,11 @@ <body> <div> - <input type="button" id="enter" value="发消息给Native" onclick="testClick();"/> + <input type="button" id="test1" value="发消息给Native" onclick="testClick();"/> + <input type="button" id="test2" value="原生不支持的指令" onclick="testClick1();"/> <div> - <a href="qmui://tab?mode=2&name=你好">scheme 跳转新界面</a> + <a href="qmui://tab?mode=2&name=你好">scheme 跳转到 Tab</a> </div> </div> @@ -26,19 +27,60 @@ var messageConsoleBox = document.getElementById("message_console"); function testClick() { - var data = {id: 1, info: "来自 Webview 的消息"}; - window.QMUIBridge.send(data, function (responseData) { - var text = document.createElement('p'); - text.innerText = "code = " + responseData.code + "; message = " + responseData.message; - messageConsoleBox.appendChild(text); - }); + window.QMUIBridge.isCmdSupport("test", function(support){ + if(support){ + var data = {cmd: "test", id: 1, info: "来自 Webview 的消息"}; + window.QMUIBridge.send(data, function (responseData) { + var text = document.createElement('p'); + text.innerText = "code = " + responseData.code + "; message = " + responseData.message; + messageConsoleBox.appendChild(text); + }); + }else{ + var text = document.createElement('p'); + text.innerText = "cmd (test) is not supported. " + messageConsoleBox.appendChild(text); + } + }) + + } + + function testClick1() { + window.QMUIBridge.isCmdSupport("test2", function(support){ + if(support){ + var data = {cmd: "test2", id: 1, info: "来自 Webview 的消息"}; + window.QMUIBridge.send(data, function (responseData) { + var text = document.createElement('p'); + text.innerText = "code = " + responseData.code + "; message = " + responseData.message; + messageConsoleBox.appendChild(text); + }); + }else{ + var text = document.createElement('p'); + text.innerText = "cmd (test2) is not supported. " + messageConsoleBox.appendChild(text); + } + }) } document.addEventListener('QMUIBridgeReady', function () { var text = document.createElement('p') text.innerText = "Bridge加载完成。" messageConsoleBox.appendChild(text) + + window.QMUIBridge.getSupportedCmdList(function(data){ + var text = document.createElement('p') + var cmdList = "原生支持的指令:" + if(data && data.length){ + for(var i = 0; i< data.length; i++){ + cmdList += data[i] + if(i < data.length - 1){ + cmdList += ", " + } + } + } + text.innerText = cmdList + messageConsoleBox.appendChild(text) + }) }, false); </script> diff --git a/qmuidemo/src/main/assets/test.png b/qmuidemo/src/main/assets/test.png new file mode 100644 index 000000000..8ce9e39cb Binary files /dev/null and b/qmuidemo/src/main/assets/test.png differ diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/QDApplication.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/QDApplication.java deleted file mode 100644 index 2e3d9b9a4..000000000 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/QDApplication.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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.Context; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.util.Log; - -import com.qmuiteam.qmui.QMUILog; -import com.qmuiteam.qmui.arch.QMUISwipeBackActivityManager; -import com.qmuiteam.qmui.qqface.QMUIQQFaceCompiler; -import com.qmuiteam.qmui.skin.QMUISkinMaker; -import com.qmuiteam.qmui.skin.QMUISkinManager; -import com.qmuiteam.qmuidemo.manager.QDSkinManager; -import com.qmuiteam.qmuidemo.manager.QDUpgradeManager; -import com.squareup.leakcanary.LeakCanary; - -import androidx.annotation.NonNull; - -/** - * Demo 的 Application 入口。 - * Created by cgine on 16/3/22. - */ -public class QDApplication extends Application { - - public static boolean openSkinMake = false; - - @SuppressLint("StaticFieldLeak") private static Context context; - - public static Context getContext() { - return context; - } - - @Override - public void onCreate() { - super.onCreate(); - context = getApplicationContext(); - if (LeakCanary.isInAnalyzerProcess(this)) { - return; - } - LeakCanary.install(this); - - QMUILog.setDelegete(new QMUILog.QMUILogDelegate() { - @Override - public void e(String tag, String msg, Object... obj) { - Log.e(tag, msg); - } - - @Override - public void w(String tag, String msg, Object... obj) { - Log.w(tag, msg); - } - - @Override - public void i(String tag, String msg, Object... obj) { - - } - - @Override - public void d(String tag, String msg, Object... obj) { - - } - - @Override - public void printErrStackTrace(String tag, Throwable tr, String format, Object... obj) { - - } - }); - - QDUpgradeManager.getInstance(this).check(); - QMUISwipeBackActivityManager.init(this); - QMUIQQFaceCompiler.setDefaultQQFaceManager(QDQQFaceManager.getInstance()); - QDSkinManager.install(this); - QMUISkinMaker.init(context, - new String[]{"com.qmuiteam.qmuidemo"}, - new String[]{"app_skin_"}, R.attr.class); - } - - @Override - public void onConfigurationChanged(@NonNull Configuration newConfig) { - super.onConfigurationChanged(newConfig); - if((newConfig.uiMode & 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); - } - } -} diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/QDApplication.kt b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/QDApplication.kt new file mode 100644 index 000000000..102cd2868 --- /dev/null +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/QDApplication.kt @@ -0,0 +1,134 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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 + } +} \ No newline at end of file diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/QDMainActivity.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/QDMainActivity.java index e66cbb53c..5cffc60f9 100644 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/QDMainActivity.java +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/QDMainActivity.java @@ -16,6 +16,9 @@ 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; @@ -31,14 +34,15 @@ 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.SwipeBackLayout; import com.qmuiteam.qmui.arch.annotation.DefaultFirstFragment; -import com.qmuiteam.qmui.arch.annotation.FirstFragments; import com.qmuiteam.qmui.arch.annotation.LatestVisitRecord; import com.qmuiteam.qmui.skin.QMUISkinHelper; -import com.qmuiteam.qmui.skin.QMUISkinMaker; import com.qmuiteam.qmui.skin.QMUISkinManager; import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; import com.qmuiteam.qmui.util.QMUIDisplayHelper; @@ -46,53 +50,18 @@ import com.qmuiteam.qmui.util.QMUIStatusBarHelper; import com.qmuiteam.qmui.util.QMUIViewOffsetHelper; import com.qmuiteam.qmui.widget.QMUIRadiusImageView2; -import com.qmuiteam.qmui.widget.QMUIWindowInsetLayout; 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.BaseFragment; import com.qmuiteam.qmuidemo.base.BaseFragmentActivity; import com.qmuiteam.qmuidemo.fragment.QDWebExplorerFragment; -import com.qmuiteam.qmuidemo.fragment.components.QDPopupFragment; -import com.qmuiteam.qmuidemo.fragment.components.swipeAction.QDRVSwipeMutiActionFragment; -import com.qmuiteam.qmuidemo.fragment.components.QDTabSegmentFixModeFragment; -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.fragment.home.HomeFragment; -import com.qmuiteam.qmuidemo.fragment.lab.QDArchSurfaceTestFragment; -import com.qmuiteam.qmuidemo.fragment.lab.QDArchTestFragment; -import com.qmuiteam.qmuidemo.fragment.lab.QDContinuousNestedScroll1Fragment; -import com.qmuiteam.qmuidemo.fragment.util.QDNotchHelperFragment; import com.qmuiteam.qmuidemo.manager.QDSkinManager; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentContainerView; - -import static com.qmuiteam.qmuidemo.fragment.QDWebExplorerFragment.EXTRA_TITLE; -import static com.qmuiteam.qmuidemo.fragment.QDWebExplorerFragment.EXTRA_URL; - -@FirstFragments( - value = { - HomeFragment.class, - QDArchTestFragment.class, - QDArchSurfaceTestFragment.class, - QDNotchHelperFragment.class, - QDWebExplorerFragment.class, - QDContinuousNestedScroll1Fragment.class, - QDTabSegmentFixModeFragment.class, - QDPullVerticalTestFragment.class, - QDPullHorizontalTestFragment.class, - QDPullRefreshAndLoadMoreTestFragment.class, - QDRVSwipeMutiActionFragment.class, - QDPopupFragment.class - }) @DefaultFirstFragment(HomeFragment.class) @LatestVisitRecord public class QDMainActivity extends BaseFragmentActivity { @@ -113,7 +82,9 @@ public void onSkinChange(QMUISkinManager skinManager, int oldSkin, int newSkin) @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setSkinManager(QMUISkinManager.defaultInstance(this)); + QMUISkinManager skinManager = QMUISkinManager.defaultInstance(this); + setSkinManager(skinManager); + mOnSkinChangeListener.onSkinChange(skinManager, -1, skinManager.getCurrentSkin()); } @Override @@ -126,21 +97,11 @@ public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); } - private void renderSkinMakerBtn() { - Fragment baseFragment = getCurrentFragment(); - if(baseFragment instanceof BaseFragment){ - if (QDApplication.openSkinMake) { - ((BaseFragment)baseFragment).openSkinMaker(); - } else { - QMUISkinMaker.getInstance().unBindAll(); - } - } - } @Override protected void onStart() { super.onStart(); - if(getSkinManager() != null){ + if (getSkinManager() != null) { getSkinManager().addSkinChangeListener(mOnSkinChangeListener); } } @@ -148,22 +109,19 @@ protected void onStart() { @Override protected void onResume() { super.onResume(); - renderSkinMakerBtn(); } @Override protected void onStop() { super.onStop(); - if(getSkinManager() != null){ + if (getSkinManager() != null) { getSkinManager().removeSkinChangeListener(mOnSkinChangeListener); } } - private void showGlobalActionPopup(View v){ + private void showGlobalActionPopup(View v) { String[] listItems = new String[]{ - "Change Skin", - QDApplication.openSkinMake ? "Close SkinMaker(Developing)" : "Open SkinMaker(Developing)", - "Export SkinMaker Result" + "Change Skin" }; List<String> data = new ArrayList<>(); @@ -173,7 +131,7 @@ private void showGlobalActionPopup(View v){ AdapterView.OnItemClickListener onItemClickListener = new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) { - if(i == 0){ + if (i == 0) { final String[] items = new String[]{"蓝色(默认)", "黑色", "白色"}; new QMUIDialog.MenuDialogBuilder(QDMainActivity.this) .addItems(items, new DialogInterface.OnClickListener() { @@ -186,11 +144,6 @@ public void onClick(DialogInterface dialog, int which) { .setSkinManager(QMUISkinManager.defaultInstance(QDMainActivity.this)) .create() .show(); - }else if(i == 1){ - QDApplication.openSkinMake = !QDApplication.openSkinMake; - renderSkinMakerBtn(); - }else if(i == 2){ - QMUISkinMaker.getInstance().export(QDMainActivity.this); } if (mGlobalAction != null) { mGlobalAction.dismiss(); @@ -233,7 +186,7 @@ public static Intent of(@NonNull Context context, class CustomRootView extends RootView { private FragmentContainerView fragmentContainer; - private QMUIRadiusImageView2 globalBtn; + private QMUIRadiusImageView2 globalBtn; private QMUIViewOffsetHelper globalBtnOffsetHelper; private int btnSize; private final int touchSlop; @@ -251,14 +204,6 @@ public CustomRootView(Context context, int fragmentContainerId) { fragmentContainer = new FragmentContainerView(context); fragmentContainer.setId(fragmentContainerId); - fragmentContainer.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) { - for (int i = 0; i < getChildCount(); i++) { - SwipeBackLayout.updateLayoutInSwipeBack(getChildAt(i)); - } - } - }); addView(fragmentContainer, new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); @@ -302,35 +247,35 @@ protected void onLayout(boolean changed, int left, int top, int right, int botto public boolean onInterceptTouchEvent(MotionEvent event) { float x = event.getX(), y = event.getY(); int action = event.getAction(); - if(action == MotionEvent.ACTION_DOWN){ + if (action == MotionEvent.ACTION_DOWN) { isTouchDownInGlobalBtn = isDownInGlobalBtn(x, y); touchDownX = lastTouchX = x; touchDownY = lastTouchY = y; - }else if(action == MotionEvent.ACTION_MOVE){ - if(!isDragging && isTouchDownInGlobalBtn){ + } 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){ + if (Math.sqrt(dx * dx + dy * dy) > touchSlop) { isDragging = true; } } - if(isDragging){ + 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){ + if (gx + dx < 0) { dx = -gx; - }else if(gx + dx + gw > w){ + } else if (gx + dx + gw > w) { dx = w - gw - gx; } - if(gy + dy < 0){ - dy = - gy; - }else if(gy + dy + gh > h){ + if (gy + dy < 0) { + dy = -gy; + } else if (gy + dy + gh > h) { dy = h - gh - gy; } globalBtnOffsetHelper.setLeftAndRightOffset( @@ -340,14 +285,14 @@ public boolean onInterceptTouchEvent(MotionEvent event) { } lastTouchX = x; lastTouchY = y; - } else if(action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP){ + } else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { isDragging = false; isTouchDownInGlobalBtn = false; } return isDragging; } - private boolean isDownInGlobalBtn(float x, float y){ + private boolean isDownInGlobalBtn(float x, float y) { return globalBtn.getLeft() < x && globalBtn.getRight() > x && globalBtn.getTop() < y && globalBtn.getBottom() > y; } @@ -356,35 +301,35 @@ private boolean isDownInGlobalBtn(float x, float y){ public boolean onTouchEvent(MotionEvent event) { float x = event.getX(), y = event.getY(); int action = event.getAction(); - if(action == MotionEvent.ACTION_DOWN){ + if (action == MotionEvent.ACTION_DOWN) { isTouchDownInGlobalBtn = isDownInGlobalBtn(x, y); touchDownX = lastTouchX = x; touchDownY = lastTouchY = y; - }else if(action == MotionEvent.ACTION_MOVE){ - if(!isDragging && isTouchDownInGlobalBtn){ + } 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){ + if (Math.sqrt(dx * dx + dy * dy) > touchSlop) { isDragging = true; } } - if(isDragging){ + 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){ + if (gx + dx < 0) { dx = -gx; - }else if(gx + dx + gw > w){ + } else if (gx + dx + gw > w) { dx = w - gw - gx; } - if(gy + dy < 0){ - dy = - gy; - }else if(gy + dy + gh > h){ + if (gy + dy < 0) { + dy = -gy; + } else if (gy + dy + gh > h) { dy = h - gh - gy; } globalBtnOffsetHelper.setLeftAndRightOffset( @@ -394,7 +339,7 @@ public boolean onTouchEvent(MotionEvent event) { } lastTouchX = x; lastTouchY = y; - } else if(action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP){ + } else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { isDragging = false; isTouchDownInGlobalBtn = false; } diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/activity/ArchTestActivity.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/activity/ArchTestActivity.java index 3693f5417..b0758277e 100644 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/activity/ArchTestActivity.java +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/activity/ArchTestActivity.java @@ -17,11 +17,12 @@ package com.qmuiteam.qmuidemo.activity; import android.os.Bundle; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; 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; @@ -31,9 +32,13 @@ import butterknife.BindView; import butterknife.ButterKnife; -@ActivityScheme(name = "arch", required = {"aa", "bb=3"}, keysWithBoolValue = {"aa"}) +@ActivityScheme(name = "arch", + useRefreshIfCurrentMatched = true, + required = {"aa", "bb=3"}, + keysWithBoolValue = {"aa"}) public class ArchTestActivity extends BaseActivity { - @BindView(R.id.topbar) QMUITopBarLayout mTopBar; + @BindView(R.id.topbar) + QMUITopBarLayout mTopBar; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -56,4 +61,5 @@ public void onClick(View v) { mTopBar.setTitle("Arch Test"); QDArchTestFragment.injectEntrance(mTopBar); } + } diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/activity/LauncherActivity.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/activity/LauncherActivity.java deleted file mode 100644 index 67d95f4ad..000000000 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/activity/LauncherActivity.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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.app.Activity; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.os.Bundle; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; - -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") -public class LauncherActivity extends Activity { - - private static final int PERMISSIONS_REQUEST_CODE = 10; - private static final String[] PERMISSIONS_REQUIRED = {Manifest.permission.WRITE_EXTERNAL_STORAGE}; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if ((getIntent().getFlags() & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) != 0) { - finish(); - return; - } - - if (allPermissionsGranted()) { - doAfterPermissionsGranted(); - } else { - ActivityCompat.requestPermissions(this, PERMISSIONS_REQUIRED, PERMISSIONS_REQUEST_CODE); - } - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - if (requestCode == PERMISSIONS_REQUEST_CODE) { - if (allPermissionsGranted()) { - doAfterPermissionsGranted(); - } else { - Toast.makeText(this, "Permissions not granted by the user.", Toast.LENGTH_SHORT).show(); - finish(); - } - } - } - - - private void doAfterPermissionsGranted() { - Intent intent = QMUILatestVisit.intentOfLatestVisit(this); - if (intent == null) { - intent = new Intent(this, QDMainActivity.class); - } - startActivity(intent); - finish(); - } - - - private boolean allPermissionsGranted() { - for (String permission : PERMISSIONS_REQUIRED) { - if (ContextCompat.checkSelfPermission(getBaseContext(), permission) != PackageManager.PERMISSION_GRANTED) { - return false; - } - } - return true; - } - - @Override - public void finish() { - super.finish(); - overridePendingTransition(0, 0); - } -} diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/activity/LauncherActivity.kt b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/activity/LauncherActivity.kt new file mode 100644 index 000000000..d88aa1841 --- /dev/null +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/activity/LauncherActivity.kt @@ -0,0 +1,63 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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) + } +} \ No newline at end of file diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/activity/QDPhotoPickerActivity.kt b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/activity/QDPhotoPickerActivity.kt new file mode 100644 index 000000000..3bb3fc360 --- /dev/null +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/activity/QDPhotoPickerActivity.kt @@ -0,0 +1,7 @@ +package com.qmuiteam.qmuidemo.activity + +import com.qmuiteam.photo.activity.QMUIPhotoPickerActivity + +class QDPhotoPickerActivity: QMUIPhotoPickerActivity() { + +} \ No newline at end of file diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/base/BaseFragment.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/base/BaseFragment.java index ed992c3e3..c45b7a20b 100644 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/base/BaseFragment.java +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/base/BaseFragment.java @@ -18,81 +18,73 @@ import android.content.Context; import android.content.Intent; -import android.os.Bundle; +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.skin.QMUISkinMaker; 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.QDApplication; 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; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - /** * Created by cgspine on 2018/1/7. */ public abstract class BaseFragment extends QMUIFragment { - private int mBindId = -1; + private static final String TAG = "BaseFragment"; + public BaseFragment() { } @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - if(QDApplication.openSkinMake){ - openSkinMaker(); + 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); } - public void openSkinMaker(){ - if(mBindId < 0){ - mBindId = QMUISkinMaker.getInstance().bind(this); - } - } + @Override + public void onResume() { + super.onResume(); + QDUpgradeManager.getInstance(getContext()).runUpgradeTipTaskIfExist(getActivity()); + Log.i(TAG, getClass().getSimpleName() + " onResume"); - public void closeSkinMaker(){ - QMUISkinMaker.getInstance().unBind(mBindId); - mBindId = -1; } @Override - public void onDestroyView() { - super.onDestroyView(); - closeSkinMaker(); + public void onStart() { + super.onStart(); + Log.i(TAG, getClass().getSimpleName() + " onStart"); } @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); + public void onPause() { + super.onPause(); + Log.i(TAG, getClass().getSimpleName() + " onPause"); } @Override - public void onResume() { - super.onResume(); - QDUpgradeManager.getInstance(getContext()).runUpgradeTipTaskIfExist(getActivity()); - + 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) { diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/base/BaseRecyclerAdapter.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/base/BaseRecyclerAdapter.java index 8c86c53fe..1d952cb19 100644 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/base/BaseRecyclerAdapter.java +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/base/BaseRecyclerAdapter.java @@ -86,7 +86,7 @@ public boolean onLongClick(View v) { } @Override - public void onBindViewHolder(RecyclerViewHolder holder, int position) { + public void onBindViewHolder(@NonNull RecyclerViewHolder holder, int position) { bindData(holder, position, mData.get(position)); } @@ -130,7 +130,7 @@ public void setOnItemLongClickListener(OnItemLongClickListener listener) { @SuppressWarnings("SameReturnValue") abstract public int getItemLayoutId(int viewType); - abstract public void bindData(RecyclerViewHolder holder, int position, T item); + abstract public void bindData(@NonNull RecyclerViewHolder holder, int position, @NonNull T item); public interface OnItemClickListener { void onItemClick(View itemView, int pos); diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/base/ComposeBaseFragment.kt b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/base/ComposeBaseFragment.kt new file mode 100644 index 000000000..46a0a0934 --- /dev/null +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/base/ComposeBaseFragment.kt @@ -0,0 +1,48 @@ +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() +} \ No newline at end of file diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/QDAboutFragment.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/QDAboutFragment.java deleted file mode 100644 index 40983be58..000000000 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/QDAboutFragment.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.TextView; - -import com.qmuiteam.qmui.arch.QMUIFragment; -import com.qmuiteam.qmui.arch.SwipeBackLayout; -import com.qmuiteam.qmui.util.QMUIPackageHelper; -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 java.text.SimpleDateFormat; -import java.util.Locale; - -import butterknife.BindView; -import butterknife.ButterKnife; - -import static com.qmuiteam.qmuidemo.fragment.QDWebExplorerFragment.EXTRA_TITLE; -import static com.qmuiteam.qmuidemo.fragment.QDWebExplorerFragment.EXTRA_URL; - -/** - * 关于界面 - * <p> - * Created by Kayo on 2016/11/18. - */ -public class QDAboutFragment extends BaseFragment { - - @BindView(R.id.topbar) QMUITopBarLayout mTopBar; - @BindView(R.id.version) TextView mVersionTextView; - @BindView(R.id.about_list) QMUIGroupListView mAboutGroupListView; - @BindView(R.id.copyright) TextView mCopyrightTextView; - - @Override - protected View onCreateView() { - View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_about, null); - ButterKnife.bind(this, root); - - initTopBar(); - - mVersionTextView.setText(QMUIPackageHelper.getAppVersion(getContext())); - - QMUIGroupListView.newSection(getContext()) - .addItemView(mAboutGroupListView.createItemView(getResources().getString(R.string.about_item_homepage)), new View.OnClickListener() { - @Override - public void onClick(View v) { - String url = "https://qmuiteam.com/android"; - Bundle bundle = new Bundle(); - bundle.putString(EXTRA_URL, url); - bundle.putString(EXTRA_TITLE, getResources().getString(R.string.about_item_homepage)); - QMUIFragment fragment = new QDWebExplorerFragment(); - fragment.setArguments(bundle); - startFragment(fragment); - } - }) - .addItemView(mAboutGroupListView.createItemView(getResources().getString(R.string.about_item_github)), new View.OnClickListener() { - @Override - public void onClick(View v) { - String url = "https://github.com/Tencent/QMUI_Android"; - Bundle bundle = new Bundle(); - bundle.putString(EXTRA_URL, url); - bundle.putString(EXTRA_TITLE, getResources().getString(R.string.about_item_github)); - QMUIFragment fragment = new QDWebExplorerFragment(); - fragment.setArguments(bundle); - startFragment(fragment); - } - }) - .addTo(mAboutGroupListView); - - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy", Locale.CHINA); - String currentYear = dateFormat.format(new java.util.Date()); - mCopyrightTextView.setText(String.format(getResources().getString(R.string.about_copyright), currentYear)); - - return root; - } - - private void initTopBar() { - mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - popBackStack(); - } - }); - - mTopBar.setTitle(getResources().getString(R.string.about_title)); - } - - @Override - public TransitionConfig onFetchTransitionConfig() { - return SCALE_TRANSITION_CONFIG; - } - - @Override - protected SwipeBackLayout.ViewMoveAction dragViewMoveAction() { - return SwipeBackLayout.MOVE_VIEW_TOP_TO_BOTTOM; - } -} diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/QDAboutFragment.kt b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/QDAboutFragment.kt new file mode 100644 index 000000000..db3a12d31 --- /dev/null +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/QDAboutFragment.kt @@ -0,0 +1,103 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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 + } +} \ No newline at end of file diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/QDDialogFragment.kt b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/QDDialogFragment.kt new file mode 100644 index 000000000..e0204414b --- /dev/null +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/QDDialogFragment.kt @@ -0,0 +1,230 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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<String>() + 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<String>() + 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() + } + } + } + } + } +} \ No newline at end of file diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDButtonFragment.kt b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDButtonFragment.kt index c217d1845..8ffc53c2e 100644 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDButtonFragment.kt +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDButtonFragment.kt @@ -19,6 +19,7 @@ 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 @@ -30,6 +31,7 @@ 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) diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDCollapsingTopBarLayoutFragment.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDCollapsingTopBarLayoutFragment.java index f4ad86128..2b09fbb43 100644 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDCollapsingTopBarLayoutFragment.java +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDCollapsingTopBarLayoutFragment.java @@ -17,25 +17,20 @@ package com.qmuiteam.qmuidemo.fragment.components; import android.animation.ValueAnimator; - -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.View; -import com.qmuiteam.qmui.layout.QMUIConstraintLayout; +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.fragment.components.viewpager.QDLazyTestObserver; -import com.qmuiteam.qmuidemo.manager.QDDataManager; 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; @@ -85,18 +80,6 @@ public void onOffsetChanged(QMUICollapsingTopBarLayout layout, int offset, float return rootView; } - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - getLazyViewLifecycleOwner().getLifecycle().addObserver( - new QDLazyTestObserver("QDCollapsingTopBar")); - } - - @Override - protected boolean translucentFull() { - return true; - } - private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDDialogFragment.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDDialogFragment.java deleted file mode 100644 index 07b7b31e9..000000000 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDDialogFragment.java +++ /dev/null @@ -1,458 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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.content.DialogInterface; -import android.text.InputType; -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.LinearLayout; -import android.widget.ListView; -import android.widget.ScrollView; -import android.widget.TextView; -import android.widget.Toast; - -import com.qmuiteam.qmui.skin.QMUISkinManager; -import com.qmuiteam.qmui.util.QMUIDisplayHelper; -import com.qmuiteam.qmui.util.QMUIKeyboardHelper; -import com.qmuiteam.qmui.util.QMUIResHelper; -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.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.CustomEffect; -import com.qmuiteam.qmuidemo.model.QDItemDescription; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import androidx.annotation.NonNull; -import androidx.appcompat.widget.AppCompatEditText; -import androidx.core.content.ContextCompat; -import butterknife.BindView; -import butterknife.ButterKnife; - -/** - * {@link QMUIDialog} 的使用示例。 - * Created by cgspine on 15/9/15. - */ -@Widget(widgetClass = QMUIDialog.class, iconRes = R.mipmap.icon_grid_dialog) -public class QDDialogFragment extends BaseFragment { - - @BindView(R.id.topbar) - QMUITopBarLayout mTopBar; - @BindView(R.id.listview) - ListView mListView; - - private QDItemDescription mQDItemDescription; - private int mCurrentDialogStyle = com.qmuiteam.qmui.R.style.QMUI_Dialog; - - @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.addRightImageButton(R.mipmap.icon_topbar_overflow, R.id.topbar_right_change_button) - .setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - showBottomSheet(); - } - }); - - mTopBar.setTitle(mQDItemDescription.getName()); - - notifyEffect(new CustomEffect("custom effect: 1")); - notifyEffect(new CustomEffect("custom effect: 2")); - notifyEffect(new CustomEffect("custom effect: 3")); - notifyEffect(new CustomEffect("custom effect: 4")); - } - - - private void initListView() { - String[] listItems = new String[]{ - "消息类型对话框(蓝色按钮)", - "消息类型对话框(红色按钮)", - "消息类型对话框 (很长文案)", - "菜单类型对话框", - "带 Checkbox 的消息确认框", - "单选菜单类型对话框", - "多选菜单类型对话框", - "多选菜单类型对话框(item 数量很多)", - "带输入框的对话框", - "高度适应键盘升降的对话框" - }; - List<String> 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: - showMessagePositiveDialog(); - break; - case 1: - showMessageNegativeDialog(); - break; - case 2: - showLongMessageDialog(); - break; - case 3: - showMenuDialog(); - break; - case 4: - showConfirmMessageDialog(); - break; - case 5: - showSingleChoiceDialog(); - break; - case 6: - showMultiChoiceDialog(); - break; - case 7: - showNumerousMultiChoiceDialog(); - break; - case 8: - showEditTextDialog(); - break; - case 9: - showAutoDialog(); - break; - } - } - }); - } - - // ================================ 生成不同类型的对话框 - private void showMessagePositiveDialog() { - new QMUIDialog.MessageDialogBuilder(getActivity()) - .setTitle("标题") - .setMessage("确定要发送吗?") - .setSkinManager(QMUISkinManager.defaultInstance(getContext())) - .addAction("取消", new QMUIDialogAction.ActionListener() { - @Override - public void onClick(QMUIDialog dialog, int index) { - dialog.dismiss(); - } - }) - .addAction(0, "确定", QMUIDialogAction.ACTION_PROP_POSITIVE, new QMUIDialogAction.ActionListener() { - @Override - public void onClick(QMUIDialog dialog, int index) { - dialog.dismiss(); - Toast.makeText(getActivity(), "发送成功", Toast.LENGTH_SHORT).show(); - } - }) - .create(mCurrentDialogStyle).show(); - } - - private void showMessageNegativeDialog() { - new QMUIDialog.MessageDialogBuilder(getActivity()) - .setTitle("标题") - .setMessage("确定要删除吗?") - .setSkinManager(QMUISkinManager.defaultInstance(getContext())) - .addAction("取消", new QMUIDialogAction.ActionListener() { - @Override - public void onClick(QMUIDialog dialog, int index) { - dialog.dismiss(); - } - }) - .addAction(0, "删除", QMUIDialogAction.ACTION_PROP_NEGATIVE, new QMUIDialogAction.ActionListener() { - @Override - public void onClick(QMUIDialog dialog, int index) { - Toast.makeText(getActivity(), "删除成功", Toast.LENGTH_SHORT).show(); - dialog.dismiss(); - } - }) - .create(mCurrentDialogStyle).show(); - } - - private void showLongMessageDialog() { - new QMUIDialog.MessageDialogBuilder(getActivity()) - .setTitle("标题") - .setSkinManager(QMUISkinManager.defaultInstance(getContext())) - .setMessage("这是一段很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长" + - "很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很" + - "很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很" + - "很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很" + - "长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长" + - "很长很长很长很长很很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长" + - "很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长" + - "很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长" + - "很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长" + - "很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长" + - "很长很长很长很长很长很长很长很长很长很长很长很长很长很长长很长的文案") - .addAction("取消", new QMUIDialogAction.ActionListener() { - @Override - public void onClick(QMUIDialog dialog, int index) { - dialog.dismiss(); - } - }) - .create(mCurrentDialogStyle).show(); - } - - private void showConfirmMessageDialog() { - new QMUIDialog.CheckBoxMessageDialogBuilder(getActivity()) - .setTitle("退出后是否删除账号信息?") - .setMessage("删除账号信息") - .setChecked(true) - .setSkinManager(QMUISkinManager.defaultInstance(getContext())) - .addAction("取消", new QMUIDialogAction.ActionListener() { - @Override - public void onClick(QMUIDialog dialog, int index) { - dialog.dismiss(); - } - }) - .addAction("退出", new QMUIDialogAction.ActionListener() { - @Override - public void onClick(QMUIDialog dialog, int index) { - dialog.dismiss(); - } - }) - .create(mCurrentDialogStyle).show(); - } - - private void showMenuDialog() { - final String[] items = new String[]{"选项1", "选项2", "选项3"}; - new QMUIDialog.MenuDialogBuilder(getActivity()) - .setSkinManager(QMUISkinManager.defaultInstance(getContext())) - .addItems(items, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Toast.makeText(getActivity(), "你选择了 " + items[which], Toast.LENGTH_SHORT).show(); - dialog.dismiss(); - } - }) - .create(mCurrentDialogStyle).show(); - } - - private void showSingleChoiceDialog() { - final String[] items = new String[]{"选项1", "选项2", "选项3"}; - final int checkedIndex = 1; - new QMUIDialog.CheckableDialogBuilder(getActivity()) - .setCheckedIndex(checkedIndex) - .setSkinManager(QMUISkinManager.defaultInstance(getContext())) - .addItems(items, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Toast.makeText(getActivity(), "你选择了 " + items[which], Toast.LENGTH_SHORT).show(); - dialog.dismiss(); - } - }) - .create(mCurrentDialogStyle).show(); - } - - private void showMultiChoiceDialog() { - final String[] items = new String[]{"选项1", "选项2", "选项3", "选项4", "选项5", "选项6"}; - final QMUIDialog.MultiCheckableDialogBuilder builder = new QMUIDialog.MultiCheckableDialogBuilder(getActivity()) - .setCheckedItems(new int[]{1, 3}) - .setSkinManager(QMUISkinManager.defaultInstance(getContext())) - .addItems(items, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - - } - }); - builder.addAction("取消", new QMUIDialogAction.ActionListener() { - @Override - public void onClick(QMUIDialog dialog, int index) { - dialog.dismiss(); - } - }); - builder.addAction("提交", new QMUIDialogAction.ActionListener() { - @Override - public void onClick(QMUIDialog dialog, int index) { - String result = "你选择了 "; - for (int i = 0; i < builder.getCheckedItemIndexes().length; i++) { - result += "" + builder.getCheckedItemIndexes()[i] + "; "; - } - Toast.makeText(getActivity(), result, Toast.LENGTH_SHORT).show(); - dialog.dismiss(); - } - }); - builder.create(mCurrentDialogStyle).show(); - } - - private void showNumerousMultiChoiceDialog() { - final String[] items = new String[]{ - "选项1", "选项2", "选项3", "选项4", "选项5", "选项6", - "选项7", "选项8", "选项9", "选项10", "选项11", "选项12", - "选项13", "选项14", "选项15", "选项16", "选项17", "选项18" - }; - final QMUIDialog.MultiCheckableDialogBuilder builder = new QMUIDialog.MultiCheckableDialogBuilder(getActivity()) - .setCheckedItems(new int[]{1, 3}) - .setSkinManager(QMUISkinManager.defaultInstance(getContext())) - .addItems(items, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - - } - }); - builder.addAction("取消", new QMUIDialogAction.ActionListener() { - @Override - public void onClick(QMUIDialog dialog, int index) { - dialog.dismiss(); - } - }); - builder.addAction("提交", new QMUIDialogAction.ActionListener() { - @Override - public void onClick(QMUIDialog dialog, int index) { - String result = "你选择了 "; - for (int i = 0; i < builder.getCheckedItemIndexes().length; i++) { - result += "" + builder.getCheckedItemIndexes()[i] + "; "; - } - Toast.makeText(getActivity(), result, Toast.LENGTH_SHORT).show(); - dialog.dismiss(); - } - }); - builder.create(mCurrentDialogStyle).show(); - } - - private void showEditTextDialog() { - final QMUIDialog.EditTextDialogBuilder builder = new QMUIDialog.EditTextDialogBuilder(getActivity()); - builder.setTitle("标题") - .setSkinManager(QMUISkinManager.defaultInstance(getContext())) - .setPlaceholder("在此输入您的昵称") - .setInputType(InputType.TYPE_CLASS_TEXT) - .addAction("取消", new QMUIDialogAction.ActionListener() { - @Override - public void onClick(QMUIDialog dialog, int index) { - dialog.dismiss(); - } - }) - .addAction("确定", new QMUIDialogAction.ActionListener() { - @Override - public void onClick(QMUIDialog dialog, int index) { - CharSequence text = builder.getEditText().getText(); - if (text != null && text.length() > 0) { - Toast.makeText(getActivity(), "您的昵称: " + text, Toast.LENGTH_SHORT).show(); - dialog.dismiss(); - } else { - Toast.makeText(getActivity(), "请填入昵称", Toast.LENGTH_SHORT).show(); - } - } - }) - .create(mCurrentDialogStyle).show(); - } - - private void showAutoDialog() { - QMAutoTestDialogBuilder autoTestDialogBuilder = (QMAutoTestDialogBuilder) new QMAutoTestDialogBuilder(getActivity()) - .setSkinManager(QMUISkinManager.defaultInstance(getContext())) - .addAction("取消", new QMUIDialogAction.ActionListener() { - @Override - public void onClick(QMUIDialog dialog, int index) { - dialog.dismiss(); - } - }) - .addAction("确定", new QMUIDialogAction.ActionListener() { - @Override - public void onClick(QMUIDialog dialog, int index) { - Toast.makeText(getActivity(), "你点了确定", Toast.LENGTH_SHORT).show(); - dialog.dismiss(); - } - }); - autoTestDialogBuilder.create(mCurrentDialogStyle).show(); - QMUIKeyboardHelper.showKeyboard(autoTestDialogBuilder.getEditText(), true); - } - - class QMAutoTestDialogBuilder extends QMUIDialog.AutoResizeDialogBuilder { - private EditText mEditText; - - public QMAutoTestDialogBuilder(Context context) { - super(context); - } - - public EditText getEditText() { - return mEditText; - } - - @Override - public View onBuildContent(@NonNull QMUIDialog dialog, @NonNull Context context) { - LinearLayout layout = new LinearLayout(context); - layout.setOrientation(LinearLayout.VERTICAL); - layout.setLayoutParams(new ScrollView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); - int padding = QMUIDisplayHelper.dp2px(context, 20); - layout.setPadding(padding, padding, padding, padding); - mEditText = new AppCompatEditText(context); - QMUIViewHelper.setBackgroundKeepingPadding(mEditText, QMUIResHelper.getAttrDrawable(context, R.drawable.qmui_divider_bottom_bitmap)); - mEditText.setHint("输入框"); - LinearLayout.LayoutParams editTextLP = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, QMUIDisplayHelper.dpToPx(50)); - editTextLP.bottomMargin = QMUIDisplayHelper.dp2px(getContext(), 15); - mEditText.setLayoutParams(editTextLP); - layout.addView(mEditText); - TextView textView = new TextView(context); - textView.setLineSpacing(QMUIDisplayHelper.dp2px(getContext(), 4), 1.0f); - textView.setText("观察聚焦输入框后,键盘升起降下时 dialog 的高度自适应变化。\n\n" + - "QMUI Android 的设计目的是用于辅助快速搭建一个具备基本设计还原效果的 Android 项目," + - "同时利用自身提供的丰富控件及兼容处理,让开发者能专注于业务需求而无需耗费精力在基础代码的设计上。" + - "不管是新项目的创建,或是已有项目的维护,均可使开发效率和项目质量得到大幅度提升。\n\n QMUI Android 的设计目的是用于辅助快速搭建一个具备基本设计还原效果的 Android 项目。"); - textView.setTextColor(ContextCompat.getColor(getContext(), R.color.app_color_description)); - textView.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); - layout.addView(textView); - return layout; - } - } - - - private void showBottomSheet() { - new QMUIBottomSheet.BottomListSheetBuilder(getContext()) - .addItem("使用 QMUI 默认 Dialog 样式") - .addItem("自定义样式") - .setSkinManager(QMUISkinManager.defaultInstance(getContext())) - .setOnSheetItemClickListener(new QMUIBottomSheet.BottomListSheetBuilder.OnSheetItemClickListener() { - @Override - public void onClick(QMUIBottomSheet dialog, View itemView, int position, String tag) { - switch (position) { - case 0: - mCurrentDialogStyle = com.qmuiteam.qmui.R.style.QMUI_Dialog; - break; - case 1: - mCurrentDialogStyle = R.style.DialogTheme2; - break; - } - dialog.dismiss(); - } - }) - .build().show(); - } -} diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDGroupListViewFragment.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDGroupListViewFragment.java deleted file mode 100644 index d9ba67c59..000000000 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDGroupListViewFragment.java +++ /dev/null @@ -1,274 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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.CompoundButton; -import android.widget.Toast; - -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.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 androidx.core.content.ContextCompat; -import butterknife.BindView; -import butterknife.ButterKnife; - -/** - * {@link QMUIGroupListView} 的使用示例。 - * Created by Kayo on 2016/11/21. - */ - -@Widget(widgetClass = QMUIGroupListView.class, iconRes = R.mipmap.icon_grid_group_list_view) -public class QDGroupListViewFragment 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) { - popBackStack(); - } - }); - - mTopBar.setTitle(mQDItemDescription.getName()); - } - - private void initGroupListView() { - QMUICommonListItemView normalItem = mGroupListView.createItemView( - ContextCompat.getDrawable(getContext(), R.mipmap.about_logo), - "Item 1", - null, - QMUICommonListItemView.HORIZONTAL, - QMUICommonListItemView.ACCESSORY_TYPE_NONE); - normalItem.setOrientation(QMUICommonListItemView.VERTICAL); - - QMUICommonListItemView itemWithDetail = mGroupListView.createItemView( - ContextCompat.getDrawable(getContext(), R.mipmap.example_image0), - "Item 2", - null, - QMUICommonListItemView.HORIZONTAL, - QMUICommonListItemView.ACCESSORY_TYPE_NONE); - - // 去除 icon 的 tintColor 换肤设置 - QMUICommonListItemView.SkinConfig skinConfig = new QMUICommonListItemView.SkinConfig(); - skinConfig.iconTintColorRes = 0; - itemWithDetail.setSkinConfig(skinConfig); - itemWithDetail.setDetailText("在右方的详细信息"); - - QMUICommonListItemView itemWithDetailBelow = mGroupListView.createItemView("Item 3"); - itemWithDetailBelow.setOrientation(QMUICommonListItemView.VERTICAL); - itemWithDetailBelow.setDetailText("在标题下方的详细信息"); - - QMUICommonListItemView itemWithChevron = mGroupListView.createItemView("Item 4"); - itemWithChevron.setAccessoryType(QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON); - - QMUICommonListItemView itemWithSwitch = mGroupListView.createItemView("Item 5"); - itemWithSwitch.setAccessoryType(QMUICommonListItemView.ACCESSORY_TYPE_SWITCH); - itemWithSwitch.getSwitch().setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - Toast.makeText(getActivity(), "checked = " + isChecked, Toast.LENGTH_SHORT).show(); - } - }); - - QMUICommonListItemView itemWithDetailBelowWithChevron = mGroupListView.createItemView("Item 6"); - itemWithDetailBelowWithChevron.setOrientation(QMUICommonListItemView.VERTICAL); - itemWithDetailBelowWithChevron.setAccessoryType(QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON); - itemWithDetailBelowWithChevron.setDetailText("在标题下方的详细信息"); - - QMUICommonListItemView longTitleAndDetail = mGroupListView.createItemView(null, - "标题有点长;标题有点长;标题有点长;标题有点长;标题有点长;标题有点长", - "详细信息有点长; 详细信息有点长;详细信息有点长;详细信息有点长;详细信息有点长", - QMUICommonListItemView.VERTICAL, - QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON, - ViewGroup.LayoutParams.WRAP_CONTENT); - int paddingVer = QMUIDisplayHelper.dp2px(getContext(), 12); - longTitleAndDetail.setPadding(longTitleAndDetail.getPaddingLeft(), paddingVer, - longTitleAndDetail.getPaddingRight(), paddingVer); - - int height = QMUIResHelper.getAttrDimen(getContext(), com.qmuiteam.qmui.R.attr.qmui_list_item_height); - - QMUICommonListItemView itemWithDetailBelowWithChevronWithIcon = mGroupListView.createItemView( - ContextCompat.getDrawable(getContext(), R.mipmap.about_logo), - "Item 7", - "在标题下方的详细信息", - QMUICommonListItemView.VERTICAL, - QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON, - height); - - - QMUICommonListItemView itemWithCustom = mGroupListView.createItemView("右方自定义 View"); - itemWithCustom.setAccessoryType(QMUICommonListItemView.ACCESSORY_TYPE_CUSTOM); - QMUILoadingView loadingView = new QMUILoadingView(getActivity()); - itemWithCustom.addAccessoryCustomView(loadingView); - - - QMUICommonListItemView itemRedPoint1 = mGroupListView.createItemView( - ContextCompat.getDrawable(getContext(), R.mipmap.about_logo), - "红点显示在左边", - "在标题下方的详细信息", - QMUICommonListItemView.VERTICAL, - QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON, - height); - itemRedPoint1.setTipPosition(QMUICommonListItemView.TIP_POSITION_LEFT); - itemRedPoint1.showRedDot(true); - - QMUICommonListItemView itemRedPoint2 = mGroupListView.createItemView( - ContextCompat.getDrawable(getContext(), R.mipmap.about_logo), - "红点显示在右边", - "在标题下方的详细信息", - QMUICommonListItemView.VERTICAL, - QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON, - height); - itemRedPoint2.setTipPosition(QMUICommonListItemView.TIP_POSITION_RIGHT); - itemRedPoint2.showRedDot(true); - - QMUICommonListItemView itemRedPoint3 = mGroupListView.createItemView( - ContextCompat.getDrawable(getContext(), R.mipmap.about_logo), - "红点显示在左边", - "在右方的详细信息", - QMUICommonListItemView.HORIZONTAL, - QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON, - height); - itemRedPoint3.setTipPosition(QMUICommonListItemView.TIP_POSITION_LEFT); - itemRedPoint3.showRedDot(true); - - QMUICommonListItemView itemRedPoint4 = mGroupListView.createItemView( - ContextCompat.getDrawable(getContext(), R.mipmap.about_logo), - "红点显示在右边", - "在右方的详细信息", - QMUICommonListItemView.HORIZONTAL, - QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON, - height); - itemRedPoint4.setTipPosition(QMUICommonListItemView.TIP_POSITION_RIGHT); - itemRedPoint4.showRedDot(true); - - QMUICommonListItemView itemNew1 = mGroupListView.createItemView( - ContextCompat.getDrawable(getContext(), R.mipmap.about_logo), - "new 标识显示在左边", - "在标题下方的详细信息", - QMUICommonListItemView.VERTICAL, - QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON, - height); - itemNew1.setTipPosition(QMUICommonListItemView.TIP_POSITION_LEFT); - itemNew1.showNewTip(true); - - QMUICommonListItemView itemNew2 = mGroupListView.createItemView( - ContextCompat.getDrawable(getContext(), R.mipmap.about_logo), - "new 标识显示在右边", - "在标题下方的详细信息", - QMUICommonListItemView.VERTICAL, - QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON, - height); - itemNew2.setTipPosition(QMUICommonListItemView.TIP_POSITION_RIGHT); - itemNew2.showNewTip(true); - - QMUICommonListItemView itemNew3 = mGroupListView.createItemView( - ContextCompat.getDrawable(getContext(), R.mipmap.about_logo), - "new 标识显示在左边", - "在右方的详细信息", - QMUICommonListItemView.HORIZONTAL, - QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON, - height); - itemNew3.setTipPosition(QMUICommonListItemView.TIP_POSITION_LEFT); - itemNew3.showNewTip(true); - - QMUICommonListItemView itemNew4 = mGroupListView.createItemView( - ContextCompat.getDrawable(getContext(), R.mipmap.about_logo), - "new 标识显示在右边", - "在右方的详细信息", - QMUICommonListItemView.HORIZONTAL, - QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON, - height); - itemNew4.setTipPosition(QMUICommonListItemView.TIP_POSITION_RIGHT); - itemNew4.showNewTip(true); - - View.OnClickListener onClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - if (v instanceof QMUICommonListItemView) { - CharSequence text = ((QMUICommonListItemView) v).getText(); - Toast.makeText(getActivity(), text + " is Clicked", Toast.LENGTH_SHORT).show(); - if (((QMUICommonListItemView) v).getAccessoryType() == QMUICommonListItemView.ACCESSORY_TYPE_SWITCH) { - ((QMUICommonListItemView) v).getSwitch().toggle(); - } - } - } - }; - - int size = QMUIDisplayHelper.dp2px(getContext(), 20); - QMUIGroupListView.newSection(getContext()) - .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(getContext(), 16), 0) - .addTo(mGroupListView); - - QMUIGroupListView.newSection(getContext()) - .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); - } -} diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDGroupListViewFragment.kt b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDGroupListViewFragment.kt new file mode 100644 index 000000000..51e175337 --- /dev/null +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDGroupListViewFragment.kt @@ -0,0 +1,256 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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) + } +} \ No newline at end of file diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDPopupFragment.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDPopupFragment.java index 9e50082cc..093ccf154 100644 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDPopupFragment.java +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDPopupFragment.java @@ -28,13 +28,19 @@ 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; @@ -49,8 +55,6 @@ import java.util.Collections; import java.util.List; -import androidx.constraintlayout.widget.ConstraintLayout; -import androidx.core.content.ContextCompat; import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; @@ -268,6 +272,7 @@ void onClickBtn6(View v) { 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); @@ -281,8 +286,11 @@ void onClickBtn6(View v) { FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(size, size); frameLayout.addView(textView, lp); - FrameLayout editFitSystemWindowWrapped = new FrameLayout(getContext()); + 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()); @@ -311,11 +319,9 @@ void onClickBtn6(View v) { 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; @@ -326,8 +332,8 @@ void onClickBtn6(View v) { eLp.bottomMargin = mar; QMUIPopups.fullScreenPopup(getContext()) - .addView(frameLayout, QMUIFullScreenPopup.getOffsetHalfKeyboardHeightListener()) - .addView(editFitSystemWindowWrapped, eLp, QMUIFullScreenPopup.getOffsetKeyboardHeightListener()) + .addView(frameLayout) + .addView(editFitSystemWindowWrapped, eLp) .skinManager(QMUISkinManager.defaultInstance(getContext())) .onBlankClick(new QMUIFullScreenPopup.OnBlankClickListener() { @Override diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDPullRefreshFragment.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDPullRefreshFragment.java deleted file mode 100644 index f5d9ac608..000000000 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDPullRefreshFragment.java +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Toast; - -import com.qmuiteam.qmui.widget.QMUITopBarLayout; -import com.qmuiteam.qmui.widget.dialog.QMUIBottomSheet; -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.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; - -/** - * @author cginechen - * @date 2016-12-14 - */ - -@Widget(widgetClass = QMUIPullRefreshLayout.class, iconRes = R.mipmap.icon_grid_pull_refresh_layout) -public class QDPullRefreshFragment extends BaseFragment { - @BindView(R.id.topbar) - QMUITopBarLayout mTopBar; - @BindView(R.id.pull_to_refresh) - QMUIPullRefreshLayout mPullRefreshLayout; - @BindView(R.id.listview) - RecyclerView mListView; - private BaseRecyclerAdapter<String> mAdapter; - - private QDItemDescription mQDItemDescription; - - @Override - protected View onCreateView() { - View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_pull_refresh_listview, 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()); - - // 切换其他情况的按钮 - 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 initData() { - mListView.setLayoutManager(new LinearLayoutManager(getContext()) { - @Override - public RecyclerView.LayoutParams generateDefaultLayoutParams() { - return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT); - } - }); - - mAdapter = new BaseRecyclerAdapter<String>(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(); - } - }); - mListView.setAdapter(mAdapter); - onDataLoaded(); - 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() { - onDataLoaded(); - mPullRefreshLayout.finishRefresh(); - } - }, 2000); - } - }); - } - - private void onDataLoaded() { - List<String> 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 showBottomSheetList() { - new QMUIBottomSheet.BottomListSheetBuilder(getActivity()) - .addItem(getResources().getString(R.string.pull_refresh_default_offset_calculator)) - .addItem(getResources().getString(R.string.pull_refresh_follow_offset_calculator)) - .addItem(getResources().getString(R.string.pull_refresh_center_gravity_offset_calculator)) - .setOnSheetItemClickListener(new QMUIBottomSheet.BottomListSheetBuilder.OnSheetItemClickListener() { - @Override - public void onClick(QMUIBottomSheet dialog, View itemView, int position, String tag) { - dialog.dismiss(); - switch (position) { - case 0: - mPullRefreshLayout.setRefreshOffsetCalculator(new QMUIDefaultRefreshOffsetCalculator()); - break; - case 1: - mPullRefreshLayout.setRefreshOffsetCalculator(new QMUIFollowRefreshOffsetCalculator()); - break; - case 2: - mPullRefreshLayout.setRefreshOffsetCalculator(new QMUICenterGravityRefreshOffsetCalculator()); - break; - default: - break; - } - } - }) - .build() - .show(); - } -} diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDPullRefreshFragment.kt b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDPullRefreshFragment.kt new file mode 100644 index 000000000..66bb0e8ac --- /dev/null +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDPullRefreshFragment.kt @@ -0,0 +1,195 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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<String>? = 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<String>(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() + } +} \ No newline at end of file diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDSliderFragment.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDSliderFragment.java index bc5198ba9..2bad9b890 100644 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDSliderFragment.java +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDSliderFragment.java @@ -20,12 +20,11 @@ import android.view.LayoutInflater; import android.view.View; -import com.qmuiteam.qmui.skin.QMUISkinHelper; -import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; +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.qmui.widget.roundwidget.QMUIRoundButton; +import com.qmuiteam.qmuidemo.QDMainActivity; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.annotation.Widget; @@ -35,7 +34,13 @@ 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) @@ -106,4 +111,9 @@ public void onTouchDown(QMUISlider slider, int progress, int tickCount, boolean public void onTouchUp(QMUISlider slider, int progress, int tickCount) { } + + @Override + public void onLongTouch(QMUISlider slider, int progress, int tickCount) { + + } } diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDTabSegment2ScrollableModeFragment.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDTabSegment2ScrollableModeFragment.java index 5d4fcc85a..7f09b131a 100644 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDTabSegment2ScrollableModeFragment.java +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDTabSegment2ScrollableModeFragment.java @@ -16,24 +16,12 @@ package com.qmuiteam.qmuidemo.fragment.components; -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 androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; -import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager2.widget.ViewPager2; -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.QMUITopBarLayout; @@ -44,15 +32,11 @@ import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.adaptor.QDRecyclerViewAdapter; import com.qmuiteam.qmuidemo.base.BaseFragment; -import com.qmuiteam.qmuidemo.fragment.components.viewpager.QDLazyTestObserver; 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; @@ -86,12 +70,6 @@ protected View onCreateView() { return rootView; } - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - getLazyViewLifecycleOwner().getLifecycle().addObserver(new QDLazyTestObserver("QDTabSegment")); - } - private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDTabSegmentScrollableModeFragment.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDTabSegmentScrollableModeFragment.java index 44e9004f9..d1266addd 100644 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDTabSegmentScrollableModeFragment.java +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDTabSegmentScrollableModeFragment.java @@ -44,10 +44,10 @@ import com.qmuiteam.qmuidemo.QDMainActivity; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; -import com.qmuiteam.qmuidemo.fragment.components.viewpager.QDLazyTestObserver; 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; @@ -59,6 +59,7 @@ @Widget(group = Group.Other, name = "内容自适应,超过父容器则滚动") @FragmentScheme( name = "tab", + useRefreshIfCurrentMatched = true, activities = {QDMainActivity.class}, required = {"mode=2", "name"}, keysWithIntValue = {"mode"}) @@ -144,12 +145,6 @@ protected View onCreateView() { return rootView; } - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - getLazyViewLifecycleOwner().getLifecycle().addObserver(new QDLazyTestObserver("QDTabSegment")); - } - private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override @@ -235,6 +230,12 @@ 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); } @@ -289,4 +290,11 @@ public int getPosition() { return position; } } + + @Override + public void refreshFromScheme(@Nullable Bundle bundle) { + Toast.makeText(getContext(), + "refreshFromScheme: name = " + bundle.getString("name"), + Toast.LENGTH_SHORT).show(); + } } diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDTabSegmentSpaceWeightFragment.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDTabSegmentSpaceWeightFragment.java index 50b71fd1f..f27bc7c95 100644 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDTabSegmentSpaceWeightFragment.java +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDTabSegmentSpaceWeightFragment.java @@ -16,7 +16,6 @@ package com.qmuiteam.qmuidemo.fragment.components; -import android.os.Bundle; import android.util.TypedValue; import android.view.Gravity; import android.view.LayoutInflater; @@ -35,7 +34,6 @@ import com.qmuiteam.qmuidemo.QDMainActivity; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; -import com.qmuiteam.qmuidemo.fragment.components.viewpager.QDLazyTestObserver; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; @@ -45,7 +43,6 @@ import java.util.Map; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; @@ -127,12 +124,6 @@ protected View onCreateView() { return rootView; } - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - getLazyViewLifecycleOwner().getLifecycle().addObserver(new QDLazyTestObserver("QDTabSegment")); - } - private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDVerticalTextViewFragment.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDVerticalTextViewFragment.java index bc85dff2e..44f540544 100644 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDVerticalTextViewFragment.java +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDVerticalTextViewFragment.java @@ -45,7 +45,6 @@ public class QDVerticalTextViewFragment extends BaseFragment { @BindView(R.id.verticalTextView_editText) EditText mEditText; - @Override protected View onCreateView() { View rootView = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_verticaltextview, null); @@ -53,7 +52,6 @@ protected View onCreateView() { initTopBar(); initVerticalTextView(); - return rootView; } diff --git a/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeParamValueDecoder.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/SliderSchemeMatcher.java similarity index 54% rename from arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeParamValueDecoder.java rename to qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/SliderSchemeMatcher.java index e94cc3c2d..51b001b29 100644 --- a/arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeParamValueDecoder.java +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/SliderSchemeMatcher.java @@ -14,30 +14,28 @@ * limitations under the License. */ -package com.qmuiteam.qmui.arch.scheme; +package com.qmuiteam.qmuidemo.fragment.components; -import android.app.Activity; -import android.net.Uri; - -import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.qmuiteam.qmui.arch.scheme.QMUIDefaultSchemeMatcher; +import com.qmuiteam.qmui.arch.scheme.SchemeItem; + import java.util.Map; -public class QMUISchemeParamValueDecoder implements QMUISchemeHandleInterpolator { +public class SliderSchemeMatcher extends QMUIDefaultSchemeMatcher { @Override - public boolean intercept(@NonNull QMUISchemeHandler schemeHandler, - @NonNull Activity activity, - @NonNull String action, - @Nullable Map<String, String> params, - @NonNull String origin) { + public boolean match(SchemeItem schemeItem, @Nullable Map<String, String> params) { if (params != null) { - for (String key : params.keySet()) { - String oldValue = params.get(key); - if (oldValue != null && oldValue.length() > 0) { - params.put(key, Uri.decode(oldValue)); + try { + String modeStr = params.get("mode"); + if (modeStr != null && !modeStr.isEmpty()) { + int mode = Integer.parseInt(modeStr); + return mode > 4; } + } catch (Throwable ignore) { + } } return false; diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/qqface/QDQQFaceUsageFragment.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/qqface/QDQQFaceUsageFragment.java deleted file mode 100644 index f48e51921..000000000 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/qqface/QDQQFaceUsageFragment.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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.Gravity; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.Toast; - -import com.qmuiteam.qmui.arch.annotation.LatestVisitRecord; -import com.qmuiteam.qmui.qqface.QMUIQQFaceView; -import com.qmuiteam.qmui.span.QMUITouchableSpan; -import com.qmuiteam.qmui.util.QMUIDisplayHelper; -import com.qmuiteam.qmui.widget.QMUITopBar; -import com.qmuiteam.qmui.widget.QMUITopBarLayout; -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 androidx.core.content.ContextCompat; -import butterknife.BindView; -import butterknife.ButterKnife; - -/** - * @author cginechen - * @date 2016-12-24 - */ - -@Widget(group = Group.Other, name = "QQ表情使用展示") -@LatestVisitRecord -public class QDQQFaceUsageFragment extends BaseFragment { - @BindView(R.id.topbar) QMUITopBarLayout mTopBar; - @BindView(R.id.qqface1) QMUIQQFaceView mQQFace1; - @BindView(R.id.qqface2) QMUIQQFaceView mQQFace2; - @BindView(R.id.qqface3) QMUIQQFaceView mQQFace3; - @BindView(R.id.qqface4) QMUIQQFaceView mQQFace4; - @BindView(R.id.qqface5) QMUIQQFaceView mQQFace5; - @BindView(R.id.qqface6) QMUIQQFaceView mQQFace6; - @BindView(R.id.qqface7) QMUIQQFaceView mQQFace7; - @BindView(R.id.qqface8) QMUIQQFaceView mQQFace8; - @BindView(R.id.qqface9) QMUIQQFaceView mQQFace9; - @BindView(R.id.qqface10) QMUIQQFaceView mQQFace10; - @BindView(R.id.qqface11) QMUIQQFaceView mQQFace11; - @BindView(R.id.qqface12) QMUIQQFaceView mQQFace12; - @BindView(R.id.qqface13) QMUIQQFaceView mQQFace13; - @BindView(R.id.qqface14) QMUIQQFaceView mQQFace14; - @BindView(R.id.qqface15) QMUIQQFaceView mQQFace15; - @BindView(R.id.qqface16) QMUIQQFaceView mQQFace16; - @BindView(R.id.qqface17) QMUIQQFaceView mQQFace17; - @BindView(R.id.qqface18) QMUIQQFaceView mQQFace18; - - @Override - protected View onCreateView() { - View view = LayoutInflater.from(getContext()).inflate(R.layout.fragment_qqface_layout, null); - ButterKnife.bind(this, view); - initTopBar(); - initData(); - return view; - } - - 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 initData() { - mQQFace1.setText("这是一行很长很长[微笑][微笑][微笑][微笑]的文本,但是[微笑][微笑][微笑][微笑]只能单行显示"); - mQQFace2.setText("这是一段很长很长[微笑][微笑][微笑][微笑]的文本,但是最多只能显示三行;" + - "这是一段很长很长[微笑][微笑][微笑][微笑]的文本,但是最多只能显示三行;" + - "这是一段很长很长[微笑][微笑][微笑][微笑]的文本,但是最多只能显示三行。"); - mQQFace3.setText("这是一行很长很长[微笑][微笑][微笑][微笑]的文本,但是[微笑][微笑][微笑][微笑]只能单行显示"); - mQQFace4.setText("这是一段很长很长[微笑][微笑][微笑][微笑]的文本,但是最多只能显示三行;" + - "这是一段很长很长[微笑][微笑][微笑][微笑]的文本,但是最多只能显示三行;" + - "这是一段很长很长[微笑][微笑][微笑][微笑]的文本,但是最多只能显示三行。"); - mQQFace5.setText("这是一行很长很长[微笑][微笑][微笑][微笑]的文本,但是[微笑][微笑][微笑][微笑]只能单行显示"); - mQQFace6.setText("这是一段很长很长[微笑][微笑][微笑][微笑]的文本,但是最多只能显示三行;" + - "这是一段很长很长[微笑][微笑][微笑][微笑]的文本,但是最多只能显示三行;" + - "这是一段很长很长[微笑][微笑][微笑][微笑]的文本,但是最多只能显示三行。"); - - mQQFace7.setText("[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + - "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]"); - mQQFace8.setText("[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + - "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]"); - mQQFace9.setText("[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + - "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]"); - mQQFace10.setText("[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + - "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + - "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + - "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + - "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + - "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + - "[微笑][微笑][微笑][微笑][微笑]"); - mQQFace11.setText("[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + - "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + - "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + - "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + - "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + - "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + - "[微笑][微笑][微笑][微笑][微笑]"); - mQQFace12.setText("[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + - "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + - "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + - "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + - "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + - "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + - "[微笑][微笑][微笑][微笑][微笑]"); - mQQFace13.setText("表情可以和字体一起变大[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + - "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + - "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + - "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + - "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + - "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + - "[微笑][微笑][微笑][微笑][微笑]"); - - String topic = "#[发呆][微笑]话题"; - String text = "这是一段文本,为了测量 span 的点击在不同 Gravity 下能否正常工作。" + topic; - - - SpannableString sb = new SpannableString(text); - QMUITouchableSpan span = new 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 - public void onSpanClick(View widget) { - Toast.makeText(widget.getContext(), "点击了话题", Toast.LENGTH_SHORT).show(); - } - }; - span.setIsNeedUnderline(true); - sb.setSpan(span, text.indexOf(topic), text.indexOf(topic) + topic.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - mQQFace14.setText(sb); - mQQFace15.setText(sb); - mQQFace15.setLinkUnderLineColor(Color.RED); - mQQFace16.setText(sb); - mQQFace16.setLinkUnderLineHeight(QMUIDisplayHelper.dp2px(getContext(), 4)); - mQQFace16.setLinkUnderLineColor(ContextCompat.getColorStateList(getContext(), R.color.s_app_color_blue_to_red)); - mQQFace15.setGravity(Gravity.CENTER); - mQQFace16.setGravity(Gravity.RIGHT); - - mQQFace17.setLinkUnderLineColor(Color.RED); - mQQFace17.setNeedUnderlineForMoreText(true); - mQQFace17.setText("这是一段文本,为了测量更多更多更多更多更多更多更多更多更多更多更多更多更多更多更多" + - "更多更多更多更多更多更多更多更多更多更多更多更多更多更多更多更多更多更多更多更多更多更多更多更多" + - "更多更多更多更多更多更多更多更多更多更多更多更多更多更多更多的显示情况"); - - mQQFace18.setParagraphSpace(QMUIDisplayHelper.dp2px(getContext(), 20)); - mQQFace18.setText("这是一段文本,为[微笑]了测量多段落[微笑]\n" + - "这是一段文本,为[微笑]了测量多段落[微笑]\n这是一段文本,为[微笑]了测量多段落[微笑]"); - } -} diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/qqface/QDQQFaceUsageFragment.kt b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/qqface/QDQQFaceUsageFragment.kt new file mode 100644 index 000000000..26647ebfd --- /dev/null +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/qqface/QDQQFaceUsageFragment.kt @@ -0,0 +1,393 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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<Pair<Int, Int>>() + 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() + } +} \ No newline at end of file diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/qqface/QMUIQQFaceView2.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/qqface/QMUIQQFaceView2.java deleted file mode 100644 index dd9cef2fd..000000000 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/qqface/QMUIQQFaceView2.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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.Canvas; -import android.graphics.Color; -import android.graphics.Typeface; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.view.View; - -import androidx.annotation.Nullable; - -import com.qmuiteam.qmui.type.LineLayout; -import com.qmuiteam.qmui.type.TypeEnvironment; -import com.qmuiteam.qmui.type.TypeModel; -import com.qmuiteam.qmui.type.parser.EmojiTextParser; -import com.qmuiteam.qmui.type.parser.TextParser; -import com.qmuiteam.qmui.util.QMUIDisplayHelper; -import com.qmuiteam.qmuidemo.QDQQFaceManager; - -public class QMUIQQFaceView2 extends View { - - private TypeEnvironment mTypeEnvironment = new TypeEnvironment(); - private LineLayout mLineLayout = new LineLayout(mTypeEnvironment) - .setCalculateWholeLines(true) - .setEllipsize(TextUtils.TruncateAt.MIDDLE) - .setMoreText("更多", Color.RED, Typeface.DEFAULT_BOLD) - .setMaxLines(10); - private TextParser mTextParser = new EmojiTextParser(QDQQFaceManager.getInstance()); - - public QMUIQQFaceView2(Context context) { - this(context, null); - } - - public QMUIQQFaceView2(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - mTypeEnvironment.setTextSize(QMUIDisplayHelper.dp2px(context, 16)); - mTypeEnvironment.setLineSpace(QMUIDisplayHelper.dp2px(context, 5)); - mTypeEnvironment.setParagraphSpace(QMUIDisplayHelper.dp2px(context, 15)); - mTypeEnvironment.setAlignment(TypeEnvironment.Alignment.JUSTIFY); - TypeModel typeModel = mTextParser.parse("QMUI 换肤最原始是为了适配 Dark Mode。" + - "但作为框架的实现者,就需要考虑到更通用的使用形式,并且要尽可能保证 API 的简" + - "洁性。因而 QMUI 是支持多套肤色的切换,而 [微笑]Dark Mode 只是其中的一种。\n" + - "在无需重启 Activity 的前提下,我们做[流泪]换肤框架的实现思路其实是很简单的:" + - "就是当触发换肤时,遍历 View 树 来更新 View 的肤色相关的属性。基于这一" + - "思路,组件的意义就在于利用数据结构、设[色]计模式、系统 API等来简化封装出一套" + - "足够方便的使用接口,避免业务使用时为了完成功[大哭]能而堆砌一堆 if else 代码。" + - "写组件也不是一个高大尚的事情,在业务开发过程中,我们应该尽可能多思考,构建一" + - "些好用的组件,持续锻炼,才能逐渐 Hold 住越来越强大的组件。\n此外,在业务开发之" + - "余,我们需要多读一些源码,如果你一直[大哭]走业务线,可能不会发[大哭]觉阅读源码的作用,而" + - "如果你有尝试封装组件,那[大哭]么这些优秀的库往[大哭]往会给你思路的启迪。如果是两年前的我" + - "来写换肤框架,我写出来的框[大哭]架可能比现在差得很远。如果你阅[大哭]读本文,也期望能给你以" + - "启迪。"); - mLineLayout.setTypeModel(typeModel); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int widthSize = MeasureSpec.getSize(widthMeasureSpec); - int heightSize = MeasureSpec.getSize(heightMeasureSpec); - mTypeEnvironment.setMeasureLimit(widthSize, heightSize); - mLineLayout.measureAndLayout(); - setMeasuredDimension(widthSize, mLineLayout.getContentHeight()); - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - mLineLayout.draw(canvas); - } -} diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/section/QDGridSectionAdapter.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/section/QDGridSectionAdapter.java index d38871cf3..168683132 100644 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/section/QDGridSectionAdapter.java +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/section/QDGridSectionAdapter.java @@ -27,6 +27,7 @@ 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; @@ -37,6 +38,13 @@ public class QDGridSectionAdapter extends QMUIDefaultStickySectionAdapter<SectionHeader, SectionItem> { + public QDGridSectionAdapter() { + } + + public QDGridSectionAdapter(boolean removeSectionTitleIfOnlyOneSection) { + super(removeSectionTitleIfOnlyOneSection); + } + @NonNull @Override protected ViewHolder onCreateSectionHeaderViewHolder(@NonNull ViewGroup viewGroup) { diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/section/QDListSectionAdapter.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/section/QDListSectionAdapter.java index adf9d29b0..657649ff8 100644 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/section/QDListSectionAdapter.java +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/section/QDListSectionAdapter.java @@ -29,6 +29,13 @@ public class QDListSectionAdapter extends QDGridSectionAdapter { + public QDListSectionAdapter() { + } + + public QDListSectionAdapter(boolean removeSectionTitleIfOnlyOneSection) { + super(removeSectionTitleIfOnlyOneSection); + } + @NonNull @Override protected ViewHolder onCreateSectionItemViewHolder(@NonNull ViewGroup viewGroup) { diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/section/QDListSectionLayoutFragment.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/section/QDListSectionLayoutFragment.java index 90156436c..262abcb7c 100644 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/section/QDListSectionLayoutFragment.java +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/section/QDListSectionLayoutFragment.java @@ -31,7 +31,7 @@ public class QDListSectionLayoutFragment extends QDBaseSectionLayoutFragment { @Override protected QMUIStickySectionAdapter<SectionHeader, SectionItem, QMUIStickySectionAdapter.ViewHolder> createAdapter() { - return new QDListSectionAdapter(); + return new QDListSectionAdapter(true); } @Override diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/viewpager/QDFitSystemWindowViewPagerFragment.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/viewpager/QDFitSystemWindowViewPagerFragment.java index 73369fc08..368996fc7 100644 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/viewpager/QDFitSystemWindowViewPagerFragment.java +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/viewpager/QDFitSystemWindowViewPagerFragment.java @@ -54,12 +54,6 @@ protected View onCreateView() { return layout; } - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - getLazyViewLifecycleOwner().getLifecycle().addObserver(new QDLazyTestObserver("QDfSWViewPager")); - } - private void initPagers() { QMUIFragmentPagerAdapter pagerAdapter = new QMUIFragmentPagerAdapter(getChildFragmentManager()) { @Override diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/viewpager/QDLazyTestObserver.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/viewpager/QDLazyTestObserver.java deleted file mode 100644 index 7f5e194d7..000000000 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/viewpager/QDLazyTestObserver.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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.util.Log; - -import androidx.lifecycle.Lifecycle; -import androidx.lifecycle.LifecycleObserver; -import androidx.lifecycle.LifecycleOwner; -import androidx.lifecycle.OnLifecycleEvent; - -public class QDLazyTestObserver implements LifecycleObserver { - private static final String TAG = "QDLazyTestObserver"; - private final String mPrefix; - - public QDLazyTestObserver(String prefix) { - mPrefix = prefix; - } - - @OnLifecycleEvent(Lifecycle.Event.ON_CREATE) - void onCreate(LifecycleOwner owner) { - Log.i(TAG, mPrefix + ": onCreate"); - } - - @OnLifecycleEvent(Lifecycle.Event.ON_START) - void onStart(LifecycleOwner owner) { - Log.i(TAG, mPrefix + ": onStart"); - } - - @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) - void onResume(LifecycleOwner owner) { - Log.i(TAG, mPrefix + ": onResume"); - } - - @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) - void onPause(LifecycleOwner owner) { - Log.i(TAG, mPrefix + ": onPause"); - } - - @OnLifecycleEvent(Lifecycle.Event.ON_STOP) - void onStop(LifecycleOwner owner) { - Log.i(TAG, mPrefix + ": onStop"); - } - - @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) - void onDestroy(LifecycleOwner owner) { - Log.i(TAG, mPrefix + ": onDestroy"); - } -} diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/viewpager/QDViewPagerFragment.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/viewpager/QDViewPagerFragment.java index 3c835a36c..ac8ac1dd3 100644 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/viewpager/QDViewPagerFragment.java +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/viewpager/QDViewPagerFragment.java @@ -16,7 +16,6 @@ package com.qmuiteam.qmuidemo.fragment.components.viewpager; -import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -29,8 +28,6 @@ import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; -import androidx.annotation.Nullable; -import androidx.lifecycle.LifecycleObserver; import butterknife.BindView; import butterknife.ButterKnife; @@ -63,12 +60,6 @@ protected View onCreateView() { return root; } - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - getLazyViewLifecycleOwner().getLifecycle().addObserver(new QDLazyTestObserver("QDViewPager")); - } - private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/home/HomeController.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/home/HomeController.java index e01aa1464..c6431b565 100644 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/home/HomeController.java +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/home/HomeController.java @@ -21,12 +21,15 @@ import android.content.Intent; import android.os.Parcelable; import android.util.SparseArray; -import android.view.LayoutInflater; 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.qmui.widget.QMUIWindowInsetLayout; import com.qmuiteam.qmuidemo.QDMainActivity; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; @@ -39,22 +42,15 @@ import java.util.List; -import androidx.recyclerview.widget.GridLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import butterknife.BindView; -import butterknife.ButterKnife; - /** * @author cginechen * @date 2016-10-20 */ -public abstract class HomeController extends QMUIWindowInsetLayout { +public abstract class HomeController extends LinearLayout { - @BindView(R.id.topbar) - QMUITopBarLayout mTopBar; - @BindView(R.id.recyclerView) - RecyclerView mRecyclerView; + protected QMUITopBarLayout mTopBar; + protected RecyclerView mRecyclerView; private HomeControlListener mHomeControlListener; private ItemAdapter mItemAdapter; @@ -62,8 +58,14 @@ public abstract class HomeController extends QMUIWindowInsetLayout { public HomeController(Context context) { super(context); - LayoutInflater.from(context).inflate(R.layout.home_layout, this); - ButterKnife.bind(this); + 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(); } diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/home/HomeFragment.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/home/HomeFragment.java index 96de9670a..b7d0976db 100644 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/home/HomeFragment.java +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/home/HomeFragment.java @@ -17,6 +17,7 @@ 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; @@ -74,7 +75,7 @@ public int getCount() { @Override public Object instantiateItem(final ViewGroup container, int position) { - HomeController page = mPages.get(Pager.getPagerFromPositon(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; @@ -150,6 +151,7 @@ protected View onCreateView() { 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); @@ -204,7 +206,7 @@ public void startFragment(BaseFragment fragment) { enum Pager { COMPONENT, UTIL, LAB; - public static Pager getPagerFromPositon(int position) { + public static Pager getPagerFromPosition(int position) { switch (position) { case 0: return COMPONENT; @@ -227,4 +229,4 @@ protected boolean canDragBack() { public Object onLastFragmentFinish() { return null; } -} \ No newline at end of file +} diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDArchNavFragment.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDArchNavFragment.java index b1e9a8270..0fa3ef48d 100644 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDArchNavFragment.java +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDArchNavFragment.java @@ -1,19 +1,28 @@ 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"; - @Nullable public static QMUINavFragment getInstance(Class<? extends QMUIFragment> firstClass, @Nullable Bundle bundle) { QMUINavFragment navFragment = new QDArchNavFragment(); navFragment.setArguments(initArguments(firstClass, bundle)); @@ -33,6 +42,22 @@ public void onCreate(@Nullable Bundle savedInstanceState) { } } + @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"); @@ -42,4 +67,12 @@ public void onCollectLatestVisitArgument(RecordArgumentEditor editor) { 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); + } } diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDArchTestFragment.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDArchTestFragment.java index 8c04e68e9..0478dcafb 100644 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDArchTestFragment.java +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDArchTestFragment.java @@ -68,6 +68,8 @@ public class QDArchTestFragment extends BaseFragment { QMUIRoundButton mBtn1; @BindView(R.id.btn_2) QMUIRoundButton mBtn2; + @BindView(R.id.btn_3) + QMUIRoundButton mBtn3; private Holder mHolder = new Holder(); @@ -119,6 +121,15 @@ public void onClick(View v) { startFragment(fragment); } }); + + mBtn3.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if(getParentFragment() instanceof QMUIFragment){ + ((QMUIFragment)getParentFragment()).startFragment(newInstance(next)); + } + } + }); return view; } diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDComposeTipFragment.kt b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDComposeTipFragment.kt new file mode 100644 index 000000000..049707453 --- /dev/null +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDComposeTipFragment.kt @@ -0,0 +1,122 @@ +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) + } + ) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDEditorFragment.kt b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDEditorFragment.kt new file mode 100644 index 000000000..be5591d66 --- /dev/null +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDEditorFragment.kt @@ -0,0 +1,104 @@ +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<EditorBehavior>() + } + 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() + } + } + } +} \ No newline at end of file diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDEmojiInputFragment.kt b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDEmojiInputFragment.kt new file mode 100644 index 000000000..ab663b111 --- /dev/null +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDEmojiInputFragment.kt @@ -0,0 +1,107 @@ +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) + } + } +} \ No newline at end of file diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDPhotoClipFragment.kt b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDPhotoClipFragment.kt new file mode 100644 index 000000000..660abefa2 --- /dev/null +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDPhotoClipFragment.kt @@ -0,0 +1,85 @@ +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<Bitmap?>(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 = "") + } + + } + } +} \ No newline at end of file diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDPhotoFragment.kt b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDPhotoFragment.kt new file mode 100644 index 000000000..b1b184678 --- /dev/null +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDPhotoFragment.kt @@ -0,0 +1,379 @@ +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<QMUIPhotoPickResult?>(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<Bitmap?>(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 = "") + } + } +} \ No newline at end of file diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDWebViewBridgeFragment.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDWebViewBridgeFragment.java index 2faa9964f..6463ab3c2 100644 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDWebViewBridgeFragment.java +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDWebViewBridgeFragment.java @@ -42,6 +42,9 @@ 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 { @@ -98,8 +101,16 @@ public void onHideCustomView() { @Override protected QMUIWebViewClient getWebViewClient() { QMUIWebViewBridgeHandler handler = new QMUIWebViewBridgeHandler(mWebView) { + + @Override + protected List<String> getSupportedCmdList() { + List<String> ret = new ArrayList<>(); + ret.add("test"); + return ret; + } + @Override - protected JSONObject handleMessage(String message) { + protected void handleMessage(String message, MessageFinishCallback callback) { try { JSONObject json = new JSONObject(message); String id = json.getString("id"); @@ -108,12 +119,13 @@ protected JSONObject handleMessage(String message) { JSONObject result = new JSONObject(); result.put("code", 100); result.put("message", "Native 的执行结果"); - return result; + callback.finish(result); } catch (JSONException e) { e.printStackTrace(); + callback.finish(null); } - return null; } + }; return new QMUIBridgeWebViewClient(needDispatchSafeAreaInset(), false, handler){ @Override diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/util/QDNotchHelperFragment.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/util/QDNotchHelperFragment.java index fa4868111..2a8706d0a 100644 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/util/QDNotchHelperFragment.java +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/util/QDNotchHelperFragment.java @@ -17,7 +17,6 @@ package com.qmuiteam.qmuidemo.fragment.util; import android.app.Activity; -import android.os.Build; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; @@ -26,9 +25,14 @@ 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.QMUIResHelper; 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; @@ -39,9 +43,6 @@ import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; -import androidx.core.view.ViewCompat; import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; @@ -83,6 +84,11 @@ protected View onCreateView() { 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) { @@ -153,15 +159,10 @@ private void changeToFullScreen() { } View decorView = window.getDecorView(); int systemUi = decorView.getSystemUiVisibility(); - systemUi |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - systemUi |= View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_FULLSCREEN - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - systemUi |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; - } + 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); @@ -179,14 +180,9 @@ private void changeToNotFullScreen() { } final View decorView = window.getDecorView(); int systemUi = decorView.getSystemUiVisibility(); - systemUi &= ~View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - systemUi &= ~View.SYSTEM_UI_FLAG_FULLSCREEN; - systemUi |= View.SYSTEM_UI_FLAG_LAYOUT_STABLE; - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - systemUi &= ~View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; - } + 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); diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/QDAppGlideModule.kt b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/QDAppGlideModule.kt new file mode 100644 index 000000000..bb556eb7d --- /dev/null +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/QDAppGlideModule.kt @@ -0,0 +1,8 @@ +package com.qmuiteam.qmuidemo.manager + +import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.module.AppGlideModule + +@GlideModule +class QDAppGlideModule: AppGlideModule() { +} \ No newline at end of file diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/QDDataManager.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/QDDataManager.java index 66735d429..f0ad469e6 100644 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/QDDataManager.java +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/QDDataManager.java @@ -17,10 +17,10 @@ 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.QDDialogFragment; import com.qmuiteam.qmuidemo.fragment.components.QDEmptyViewFragment; import com.qmuiteam.qmuidemo.fragment.components.QDFloatLayoutFragment; import com.qmuiteam.qmuidemo.fragment.components.QDGroupListViewFragment; @@ -33,7 +33,6 @@ 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.swipeAction.QDRVSwipeMutiActionFragment; import com.qmuiteam.qmuidemo.fragment.components.QDSliderFragment; import com.qmuiteam.qmuidemo.fragment.components.QDSpanTouchFixTextViewFragment; import com.qmuiteam.qmuidemo.fragment.components.QDTabSegmentFragment; @@ -45,7 +44,12 @@ 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; @@ -146,6 +150,11 @@ private void initLabDesc() { 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<? extends BaseFragment> cls) { diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/QDSchemeManager.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/QDSchemeManager.java deleted file mode 100644 index 832443593..000000000 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/QDSchemeManager.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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 androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.qmuiteam.qmui.arch.QMUISwipeBackActivityManager; -import com.qmuiteam.qmui.arch.scheme.QMUISchemeHandleInterpolator; -import com.qmuiteam.qmui.arch.scheme.QMUISchemeHandler; -import com.qmuiteam.qmui.arch.scheme.QMUISchemeParamValueDecoder; - -import java.util.Map; - -public class QDSchemeManager { - private static final String TAG = "QDSchemeManager"; - public static final String SCHEME_PREFIX = "qmui://"; - private static QDSchemeManager sInstance; - - public static QDSchemeManager getInstance() { - if (sInstance == null) { - sInstance = new QDSchemeManager(); - } - return sInstance; - } - - private QMUISchemeHandler mSchemeHandler; - - private QDSchemeManager() { - mSchemeHandler = new QMUISchemeHandler.Builder(SCHEME_PREFIX) - .blockSameSchemeTimeout(1000) - .addInterpolator(new QMUISchemeHandleInterpolator() { - @Override - public boolean intercept(@NonNull QMUISchemeHandler schemeHandler, - @NonNull Activity activity, - @NonNull String action, - @Nullable Map<String, String> params, - @NonNull String origin) { - // Log the scheme. - Log.i(TAG, "handle scheme: " + origin); - return false; - } - }) - .addInterpolator(new QMUISchemeParamValueDecoder()) - .build(); - } - - public boolean handle(String scheme) { - if (!mSchemeHandler.handle(scheme)) { - Log.i(TAG, "scheme can not be handled: " + scheme); - Toast.makeText(QMUISwipeBackActivityManager.getInstance().getCurrentActivity(), - "scheme can not be handled: " + scheme, Toast.LENGTH_SHORT).show(); - return false; - } - return true; - } -} diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/QDSchemeManager.kt b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/QDSchemeManager.kt new file mode 100644 index 000000000..05df4596c --- /dev/null +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/QDSchemeManager.kt @@ -0,0 +1,81 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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<SchemeInfo> + ): 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<String>): 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 + } +} \ No newline at end of file diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/QDSkinManager.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/QDSkinManager.java index 61361013e..ea2eff707 100644 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/QDSkinManager.java +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/QDSkinManager.java @@ -17,7 +17,6 @@ import android.content.Context; import android.content.res.Configuration; -import android.util.Log; import com.qmuiteam.qmui.skin.QMUISkinManager; import com.qmuiteam.qmuidemo.QDApplication; diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/QDUpgradeManager.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/QDUpgradeManager.java index ce4e7a56f..251c66183 100644 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/QDUpgradeManager.java +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/QDUpgradeManager.java @@ -51,7 +51,10 @@ public class QDUpgradeManager { 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; - private static final int sCurrentVersion = VERSION_2_0_0_alpha9; + 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; diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/UpgradeTipTask.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/UpgradeTipTask.java index feba6c143..f5ea53a74 100644 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/UpgradeTipTask.java +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/UpgradeTipTask.java @@ -19,11 +19,12 @@ import android.app.Activity; import android.content.Context; import android.content.Intent; -import androidx.core.content.ContextCompat; 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; @@ -70,7 +71,20 @@ private void appendBlockSpace(Context context, SpannableStringBuilder builder) { public CharSequence getUpgradeWord(final Activity activity) { SpannableStringBuilder text = new SpannableStringBuilder(); - if(mNewVersion == QDUpgradeManager.VERSION_2_0_0_alpha9){ + 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"); diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/view/QDWebView.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/view/QDWebView.java index cb2d46836..2c9983632 100644 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/view/QDWebView.java +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/view/QDWebView.java @@ -22,11 +22,11 @@ import android.util.AttributeSet; import android.webkit.WebSettings; -import com.qmuiteam.qmui.BuildConfig; 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; /** @@ -60,10 +60,7 @@ protected void init(Context context) { webSettings.setDomStorageEnabled(true); webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE); webSettings.setTextZoom(100); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE); - } + webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE); String screen = QMUIDisplayHelper.getScreenWidth(context) + "x" + QMUIDisplayHelper.getScreenHeight(context); String userAgent = "QMUIDemo/" + QMUIPackageHelper.getAppVersion(context) @@ -75,17 +72,13 @@ protected void init(Context context) { } // 开启调试 - if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (BuildConfig.DEBUG) { setWebContentsDebuggingEnabled(true); } } public void exec(final String jsCode) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - evaluateJavascript(jsCode, null); - } else { - loadUrl(jsCode); - } + evaluateJavascript(jsCode, null); } @Override diff --git a/qmuidemo/src/main/res/layout/fragment_arch_test.xml b/qmuidemo/src/main/res/layout/fragment_arch_test.xml index 078cb64bf..28a7173c4 100644 --- a/qmuidemo/src/main/res/layout/fragment_arch_test.xml +++ b/qmuidemo/src/main/res/layout/fragment_arch_test.xml @@ -117,6 +117,23 @@ android:paddingTop="10dp" android:text="start nav fragment" app:qmui_isRadiusAdjustBounds="true"/> + + <com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton + android:id="@+id/btn_3" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/btn_2" + android:layout_centerHorizontal="true" + android:layout_marginLeft="?attr/qmui_content_spacing_horizontal" + android:layout_marginRight="?attr/qmui_content_spacing_horizontal" + android:layout_marginTop="20dp" + android:gravity="center" + android:paddingBottom="10dp" + android:paddingLeft="16dp" + android:paddingRight="16dp" + android:paddingTop="10dp" + android:text="parentFragment.startFragment" + app:qmui_isRadiusAdjustBounds="true"/> </RelativeLayout> <com.qmuiteam.qmui.widget.QMUITopBarLayout diff --git a/qmuidemo/src/main/res/layout/fragment_fsw_viewpager.xml b/qmuidemo/src/main/res/layout/fragment_fsw_viewpager.xml index 345ccb3be..07eee80bb 100644 --- a/qmuidemo/src/main/res/layout/fragment_fsw_viewpager.xml +++ b/qmuidemo/src/main/res/layout/fragment_fsw_viewpager.xml @@ -15,7 +15,7 @@ limitations under the License. --> -<com.qmuiteam.qmui.widget.QMUIWindowInsetLayout xmlns:android="http://schemas.android.com/apk/res/android" +<com.qmuiteam.qmui.layout.QMUIFrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" @@ -38,4 +38,4 @@ app:qmui_topDividerColor="?attr/qmui_skin_support_color_separator" android:layout_width="match_parent" android:layout_height="@dimen/home_tab_height"/> -</com.qmuiteam.qmui.widget.QMUIWindowInsetLayout> \ No newline at end of file +</com.qmuiteam.qmui.layout.QMUIFrameLayout> \ No newline at end of file diff --git a/qmuidemo/src/main/res/layout/fragment_notch.xml b/qmuidemo/src/main/res/layout/fragment_notch.xml index 9f6b97832..f5266ea55 100644 --- a/qmuidemo/src/main/res/layout/fragment_notch.xml +++ b/qmuidemo/src/main/res/layout/fragment_notch.xml @@ -15,7 +15,7 @@ limitations under the License. --> -<com.qmuiteam.qmui.widget.QMUIWindowInsetLayout xmlns:android="http://schemas.android.com/apk/res/android" +<com.qmuiteam.qmui.layout.QMUIFrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto" @@ -61,4 +61,4 @@ android:textSize="12sp"/> </FrameLayout> -</com.qmuiteam.qmui.widget.QMUIWindowInsetLayout> \ No newline at end of file +</com.qmuiteam.qmui.layout.QMUIFrameLayout> \ No newline at end of file diff --git a/qmuidemo/src/main/res/layout/fragment_qqface_layout.xml b/qmuidemo/src/main/res/layout/fragment_qqface_layout.xml index 4cc1e177d..59c1016f2 100644 --- a/qmuidemo/src/main/res/layout/fragment_qqface_layout.xml +++ b/qmuidemo/src/main/res/layout/fragment_qqface_layout.xml @@ -46,14 +46,54 @@ android:orientation="vertical" android:padding="25dp"> - <com.qmuiteam.qmuidemo.fragment.components.qqface.QMUIQQFaceView2 - android:id="@+id/test_view" + <TextView + style="@style/QDCommonTitle" + android:text="单行滚动"/> + + <com.qmuiteam.qmui.type.view.MarqueeTypeView + android:id="@+id/marquee1" + android:layout_width="match_parent" + android:layout_height="wrap_content"/> + + <com.qmuiteam.qmui.type.view.MarqueeTypeView + android:id="@+id/marquee2" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="10dp"/> + + <com.qmuiteam.qmuidemo.fragment.components.qqface.Test + style="@style/QDCommonTitle" + android:layout_marginTop="20dp"/> + + <com.qmuiteam.qmui.type.view.LineTypeView + android:id="@+id/line_type_1" + android:layout_width="match_parent" + android:layout_height="wrap_content"/> + + <TextView + style="@style/QDCommonTitle" + android:text="新排版, 序号对齐" + android:layout_marginTop="20dp"/> + + <com.qmuiteam.qmui.type.view.LineTypeView + android:id="@+id/line_type_2" + android:layout_width="match_parent" + android:layout_height="wrap_content"/> + + <TextView + style="@style/QDCommonTitle" + android:text="藏文" + android:layout_marginTop="20dp"/> + + <com.qmuiteam.qmui.type.view.LineTypeView + android:id="@+id/line_type_3" android:layout_width="match_parent" android:layout_height="wrap_content"/> <TextView style="@style/QDCommonTitle" - android:text="单行表情,末尾省略"/> + android:text="单行表情,末尾省略" + android:layout_marginTop="20dp"/> <com.qmuiteam.qmui.qqface.QMUIQQFaceView android:id="@+id/qqface1" android:layout_width="wrap_content" diff --git a/qmuidemo/src/main/res/layout/fragment_verticaltextview.xml b/qmuidemo/src/main/res/layout/fragment_verticaltextview.xml index ef33863b3..de31875b8 100644 --- a/qmuidemo/src/main/res/layout/fragment_verticaltextview.xml +++ b/qmuidemo/src/main/res/layout/fragment_verticaltextview.xml @@ -15,18 +15,29 @@ limitations under the License. --> -<com.qmuiteam.qmui.widget.QMUIWindowInsetLayout xmlns:android="http://schemas.android.com/apk/res/android" +<com.qmuiteam.qmui.layout.QMUIConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto" android:background="?attr/app_skin_common_background" app:qmui_skin_background="?attr/app_skin_common_background"> + <com.qmuiteam.qmui.widget.QMUITopBarLayout + android:id="@+id/topbar" + android:layout_width="0dp" + android:layout_height="wrap_content" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" + android:fitsSystemWindows="true"/> + <ScrollView - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_marginTop="?attr/qmui_topbar_height" - android:fitsSystemWindows="true"> + android:layout_width="0dp" + android:layout_height="0dp" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toBottomOf="@id/topbar" + app:layout_constraintBottom_toBottomOf="parent"> <LinearLayout android:layout_width="match_parent" @@ -63,10 +74,4 @@ </ScrollView> - <com.qmuiteam.qmui.widget.QMUITopBarLayout - android:id="@+id/topbar" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:fitsSystemWindows="true"/> - -</com.qmuiteam.qmui.widget.QMUIWindowInsetLayout> \ No newline at end of file +</com.qmuiteam.qmui.layout.QMUIConstraintLayout> \ No newline at end of file diff --git a/qmuidemo/src/main/res/layout/home_layout.xml b/qmuidemo/src/main/res/layout/home_layout.xml index 4f54a7355..8ca5b7716 100644 --- a/qmuidemo/src/main/res/layout/home_layout.xml +++ b/qmuidemo/src/main/res/layout/home_layout.xml @@ -15,20 +15,20 @@ --> <merge xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> - <androidx.recyclerview.widget.RecyclerView - android:id="@+id/recyclerView" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_marginTop="?attr/qmui_topbar_height" - android:fitsSystemWindows="true" /> - <com.qmuiteam.qmui.widget.QMUITopBarLayout android:id="@+id/topbar" android:layout_width="match_parent" android:layout_height="wrap_content" android:fitsSystemWindows="true"/> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/recyclerView" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1"/> </merge> \ No newline at end of file diff --git a/qmuidemo/src/main/res/values/styles.xml b/qmuidemo/src/main/res/values/styles.xml index de0a9b071..b0fea81c5 100644 --- a/qmuidemo/src/main/res/values/styles.xml +++ b/qmuidemo/src/main/res/values/styles.xml @@ -47,6 +47,8 @@ <style name="QDTopBar" parent="QMUI.TopBar"> <item name="qmui_topbar_height">48dp</item> <item name="qmui_topbar_image_btn_height">48dp</item> + <item name="qmui_topbar_title_bold">true</item> + <item name="qmui_topbar_text_btn_bold">true</item> </style> <style name="QDRoundButtonStyle" parent="@style/Button.Compat"> diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 14e022f50..000000000 --- a/settings.gradle +++ /dev/null @@ -1,3 +0,0 @@ -include ':qmuidemo', ':qmui', ':lib', ':compiler', ':lint', ':lintrule', ':arch', ':arch-compiler', - ':arch-annotation', ':skin-maker', ':skin-maker-plugin' -include ':type' diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 000000000..b38d2bcb8 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,24 @@ +pluginManagement { + repositories { + mavenCentral() + } +} + +includeBuild("./plugin") + +include(":qmuidemo") +include(":qmui") +include(":lib") +include(":compiler") +include(":arch") +include(":arch-compiler") +include(":arch-annotation") +include(":type") +include(":compose-core") +include(":compose") +include(":photo") +include(":photo-coil") +include(":photo-glide") +include(":editor") + + diff --git a/skin-maker-plugin/build.gradle b/skin-maker-plugin/build.gradle deleted file mode 100644 index 81f408613..000000000 --- a/skin-maker-plugin/build.gradle +++ /dev/null @@ -1,39 +0,0 @@ -plugins { - id 'java-gradle-plugin' - id 'groovy' - id "com.gradle.plugin-publish" version "0.10.1" -} - -version = QMUI_SKIN_MAKER_VERSION - -dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation gradleApi() - implementation localGroovy() - - implementation 'org.javassist:javassist:3.18.2-GA' - implementation 'com.android.tools.build:gradle-api:3.5.1' - implementation 'com.android.tools.build:gradle:3.5.1' - implementation 'com.squareup:javapoet:1.10.0' - implementation 'commons-io:commons-io:2.6' - - testImplementation 'junit:junit:4.12' -} - -sourceCompatibility = "1.7" -targetCompatibility = "1.7" - -gradlePlugin { - plugins { - skinMakerPlugin { - id = 'com.qmuiteam.qmui.skinMaker' - implementationClass = 'com.qmuiteam.qmui.SkinMakerPlugin' - } - } -} - - -File deployConfig = rootProject.file('gradle/deploy.properties') -if (deployConfig.exists()) { - apply from: rootProject.file('gradle/deploy.gradle') -} diff --git a/skin-maker-plugin/src/main/groovy/com/qmuiteam/qmui/SkinMakerTransform.groovy b/skin-maker-plugin/src/main/groovy/com/qmuiteam/qmui/SkinMakerTransform.groovy deleted file mode 100644 index a52028715..000000000 --- a/skin-maker-plugin/src/main/groovy/com/qmuiteam/qmui/SkinMakerTransform.groovy +++ /dev/null @@ -1,420 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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 com.android.build.api.transform.* -import com.android.build.gradle.internal.pipeline.TransformManager -import groovy.transform.stc.ClosureParams -import groovy.transform.stc.SimpleType -import javassist.* -import org.apache.commons.codec.digest.DigestUtils -import org.apache.commons.io.FileUtils -import org.gradle.api.Project -import org.gradle.api.logging.LogLevel - -class SkinMakerTransform extends Transform { - - private Project mProject - private SkinMakerPlugin.SkinMaker mSkinMaker - - SkinMakerTransform(Project project, SkinMakerPlugin.SkinMaker skinMaker) { - mProject = project - mSkinMaker = skinMaker - } - - - @Override - String getName() { - return "skin-maker" - } - - @Override - Set<QualifiedContent.ContentType> getInputTypes() { - return TransformManager.CONTENT_CLASS - } - - @Override - Set<? super QualifiedContent.Scope> getScopes() { - return TransformManager.SCOPE_FULL_PROJECT - } - - @Override - boolean isIncremental() { - return false - } - - @Override - void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { - File sourceFile = mSkinMaker.file - if (sourceFile == null || !sourceFile.exists()) { - return - } - mProject.logger.log(LogLevel.INFO, "skin code source: " + sourceFile.path) - - def injectCode = new InjectCode() - injectCode.parseFile(sourceFile) - - def androidJar = mProject.android.bootClasspath[0].toString() - - def externalDepsJars = new ArrayList<File>() - def externalDepsDirs = new ArrayList<File>() - - transformInvocation.referencedInputs.each { transformInput -> - transformInput.jarInputs.each { jar -> - externalDepsJars.add(jar.file) - } - transformInput.directoryInputs.each { directoryInput -> - externalDepsDirs.add(directoryInput.file) - } - } - - transformInvocation.outputProvider.deleteAll() - - - def inputJars = new ArrayList<File>() - transformInvocation.inputs.each { input -> - input.jarInputs.each { - inputJars.add(it.file) - } - } - - transformInvocation.inputs.each { input -> - input.directoryInputs.each { directoryInput -> - def baseDir = directoryInput.file - ClassPool pool = new ClassPool() - pool.appendSystemPath() - pool.appendClassPath(androidJar) - externalDepsJars.each { pool.appendClassPath(it.absolutePath) } - externalDepsDirs.each { pool.appendClassPath(it.absolutePath) } - inputJars.each { pool.appendClassPath(it.absolutePath) } - pool.appendClassPath(baseDir.absolutePath) - - - handleDirectionInput(injectCode, directoryInput, pool) { className, methodName, scope, ctClass -> - if (!scope.skin.isEmpty() || !scope.id.isEmpty()) { - def sb = new StringBuilder() - sb.append("public void skinMaker") - sb.append(methodName) - sb.append("(){") - scope.skin.each { codeInfo -> - sb.append(codeInfo.fieldName) - sb.append(".setTag(com.qmuiteam.qmui.R.id.qmui_skin_value, \"") - sb.append(codeInfo.code) - sb.append("\");\n") - } - scope.id.each { codeInfo -> - def atIndex = codeInfo.code.lastIndexOf("@") - def context = codeInfo.code.substring(atIndex + 1) - sb.append("{\n") - sb.append("android.view.View view = ") - sb.append(context) - sb.append(".findViewById(") - sb.append(codeInfo.fieldName) - sb.append(");\n") - sb.append("if(view != null){") - sb.append("view.setTag(com.qmuiteam.qmui.R.id.qmui_skin_value, \"") - sb.append(codeInfo.code.substring(0, atIndex)) - sb.append("\");}\n") - sb.append("}\n") - } - sb.append("}") - CtMethod newMethod = CtMethod.make(sb.toString(), ctClass) - ctClass.addMethod(newMethod) - scope.methodCreated = true - } - - if (!scope.adapter.isEmpty()) { - CtMethod ctMethod = getOrCreateMethodFromScope(ctClass, methodName, scope) - def sb = new StringBuilder() - scope.adapter.each { codeInfo -> - if (codeInfo.fieldName.startsWith("i/")) { - def idFullName = codeInfo.fieldName.substring(2) - def atIndex = codeInfo.code.indexOf("@") - sb.append("{\n") - sb.append("android.view.View view = ") - sb.append(codeInfo.code.substring(atIndex + 1)) - sb.append(".findViewById(") - sb.append(idFullName) - sb.append(");\n") - sb.append("if(view != null){") - sb.append("view.setTag(com.qmuiteam.qmui.R.id.qmui_skin_adapter, \"") - sb.append(codeInfo.code.substring(0, atIndex)) - sb.append("\");}\n") - sb.append("}\n") - } else { - sb.append(codeInfo.fieldName) - sb.append(".setTag(com.qmuiteam.qmui.R.id.qmui_skin_adapter, \"") - sb.append(codeInfo.code) - sb.append("\");") - } - } - ctMethod.insertAfter(sb.toString()) - } - } - - handleDirectionInput(injectCode, directoryInput, pool) { className, methodName, scope, ctClass -> - if (!scope.method.isEmpty()) { - CtMethod ctMethod = getOrCreateMethodFromScope(ctClass, methodName, scope) - def sb = new StringBuilder() - scope.method.each { codeInfo -> - sb.append(codeInfo.fieldName) - sb.append(".") - sb.append("skinMaker") - sb.append(codeInfo.code) - sb.append("();\n") - } - ctMethod.insertAfter(sb.toString()) - } - - if (!scope.c.isEmpty()) { - CtClass viewHolder = pool.get(methodName) - CtMethod ctMethod - try { - ctMethod = ctClass.getDeclaredMethod("onCreateViewHolder", pool.get("android.view.ViewGroup"), CtClass.intType) - } catch (NotFoundException ignore) { - ctMethod = CtNewMethod.make( - "protected " + - methodName.replace("${'$'}", ".") + - " onCreateViewHolder(android.view.ViewGroup parent, int viewType) { " + - "return super.onCreateViewHolder(parent, viewType);}", - ctClass) - ctClass.addMethod(ctMethod) - } - scope.c.each { codeInfo -> - try { - viewHolder.getDeclaredMethod("skinMaker" + codeInfo.code) - def tagIndex = codeInfo.code.lastIndexOf('_') - def tag = codeInfo.code.substring(0, tagIndex) - StringBuilder builder = new StringBuilder() - builder.append("if(\"") - builder.append(tag) - builder.append("\".equals(${'$1'}.getTag(com.qmuiteam.qmui.R.id.qmui_skin_adapter))){") - builder.append("${'$_'}.skinMaker") - builder.append(codeInfo.code) - builder.append("();") - builder.append("}") - ctMethod.insertAfter(builder.toString()) - } catch (NotFoundException ignore) { - } - } - } - - if(!scope.p.isEmpty()){ - CtMethod ctMethod - try { - ctMethod = ctClass.getDeclaredMethod("instantiateItem", pool.get("android.view.ViewGroup"), CtClass.intType) - } catch (NotFoundException ignore) { - ctMethod = CtNewMethod.make( - "protected java.lang.Object instantiateItem(android.view.ViewGroup parent, int position) { " + - "return super.instantiateItem(parent, position);}", - ctClass) - ctClass.addMethod(ctMethod) - } - - scope.p.each { codeInfo -> - try { - def objectName = codeInfo.fieldName - def objectCls = pool.get(objectName) - objectCls.getDeclaredMethod("skinMaker" + codeInfo.code) - - def tagIndex = codeInfo.code.lastIndexOf('_') - def tag = codeInfo.code.substring(0, tagIndex) - StringBuilder builder = new StringBuilder() - builder.append("if(\"") - builder.append(tag) - builder.append("\".equals(${'$1'}.getTag(com.qmuiteam.qmui.R.id.qmui_skin_adapter))){") - builder.append("if(${'$_'} instanceof ${objectName}){") - builder.append("((${objectName})${'$_'}).skinMaker") - builder.append(codeInfo.code) - builder.append("();") - builder.append("}") - builder.append("}") - ctMethod.insertAfter(builder.toString()) - } catch (NotFoundException ignore) { - } - } - } - - if (className.endsWith("Fragment")) { - CtMethod ctMethod - try { - ctMethod = ctClass.getDeclaredMethod("onViewCreated", pool.get("android.view.View")) - } catch (NotFoundException ignore) { - ctMethod = CtNewMethod.make( - "protected void onViewCreated(android.view.View rootView) { super.onViewCreated(rootView);}", ctClass) - ctClass.addMethod(ctMethod) - } - ctMethod.insertAfter("skinMaker" + className.split("\\.").last() + "();") - } else if (className.endsWith("Activity")) { - CtMethod ctMethod - try { - ctMethod = ctClass.getMethod("onCreate", pool.get("android.os.Bundle")) - } catch (NotFoundException ignore) { - ctMethod = CtNewMethod.make( - "protected void onCreate(android.os.Bundle savedInstanceState){super.onCreate(savedInstanceState);}", - ctClass) - ctClass.addMethod(ctMethod) - } - ctMethod.insertAfter("skinMaker" + className.split("\\.").last() + "();") - } - - } - - def dest = transformInvocation.outputProvider.getContentLocation( - directoryInput.name, - directoryInput.contentTypes, - directoryInput.scopes, - Format.DIRECTORY) - - FileUtils.copyDirectory(directoryInput.file, dest) - } - - //遍历jar文件 对jar不操作,但是要输出到out路径 - input.jarInputs.each { jarInput -> - def jarName = jarInput.name - def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath()) - if (jarName.endsWith(".jar")) { - jarName = jarName.substring(0, jarName.length() - 4) - } - def dest = transformInvocation.outputProvider.getContentLocation( - jarName + md5Name, - jarInput.contentTypes, - jarInput.scopes, - Format.JAR) - FileUtils.copyFile(jarInput.file, dest) - } - } - } - - static CtMethod getOrCreateMethodFromScope(CtClass ctClass, String methodName, CodeInScope scope) { - CtMethod ctMethod - if (scope.methodCreated) { - ctMethod = ctClass.getDeclaredMethod("skinMaker" + methodName) - } else { - ctMethod = CtMethod.make("public void skinMaker" + methodName + "(){}", ctClass) - ctClass.addMethod(ctMethod) - scope.methodCreated = true - } - return ctMethod - } - - - void handleDirectionInput(InjectCode injectCode, DirectoryInput directoryInput, ClassPool pool, - @ClosureParams(value = SimpleType.class, options = ["java.lang.String", "java.lang.String", "com.qmuiteam.qmui.CodeInScope", "javassist.CtClass"]) Closure closure) { - directoryInput.file.eachFileRecurse { file -> - String filePath = file.absolutePath - if (filePath.endsWith(".class")) { - def className = filePath.substring(directoryInput.file.absolutePath.length() + 1, filePath.length() - 6) - .replace('/', '.') - def codeMapForClass = injectCode.getCodeMapForClass(className) - if (codeMapForClass != null && !codeMapForClass.isEmpty()) { - CtClass ctClass = pool.getCtClass(className) - if (ctClass.isFrozen()) { - ctClass.defrost() - } - codeMapForClass.keySet().each { methodName -> - def scope = codeMapForClass.get(methodName) - closure.call(className, methodName, scope, ctClass) - } - ctClass.writeFile(directoryInput.file.absolutePath) - } - } - } - } - - - class InjectCode { - private HashMap<String, HashMap<String, CodeInScope>> mCodeMap = new HashMap<>() - - private String mCurrentClassName = null - private HashMap<String, CodeInScope> mCurrentCodes = null - - void parseFile(File file) { - file.newReader().lines().each { text -> - if (text != null) { - text = text.trim() - if (!text.isBlank()) { - if (mCurrentClassName == null) { - mCurrentClassName = text - mCurrentCodes = new HashMap<String, CodeInScope>() - mCodeMap.put(mCurrentClassName, mCurrentCodes) - } else if (text != ";") { - int start = 0 - int split = text.indexOf(",", start) - String key = text.substring(start, split) - CodeInScope scope = mCurrentCodes.get(key) - if (scope == null) { - scope = new CodeInScope() - mCurrentCodes.put(key, scope) - } - - // type - start = split + 1 - split = text.indexOf(",", start) - def type = text.substring(start, split) - - def codeInfo = new CodeInfo() - - // field name - start = split + 1 - split = text.indexOf(",", start) - codeInfo.fieldName = text.substring(start, split) - - // code - codeInfo.code = text.substring(split + 1) - if (type == "ref") { - scope.skin.add(codeInfo) - } else if (type == "method") { - scope.method.add(codeInfo) - } else if (type == "id") { - scope.id.add(codeInfo) - } else if (type == "adapter") { - scope.adapter.add(codeInfo) - } else if (type == "c") { - scope.c.add(codeInfo) - } else if(type == "p"){ - scope.p.add(codeInfo) - } - } else { - mCurrentClassName = null - mCurrentCodes = null - } - } - } - } - } - - HashMap<String, CodeInScope> getCodeMapForClass(String className) { - return mCodeMap.get(className) - } - } -} - -class CodeInfo { - String fieldName - String code -} - -class CodeInScope { - ArrayList<CodeInfo> skin = new ArrayList<>() - ArrayList<CodeInfo> method = new ArrayList<>() - ArrayList<CodeInfo> id = new ArrayList<>() - ArrayList<CodeInfo> adapter = new ArrayList<>() - ArrayList<CodeInfo> c = new ArrayList<>() - ArrayList<CodeInfo> p = new ArrayList<>() - boolean methodCreated = false -} \ No newline at end of file diff --git a/skin-maker/.gitignore b/skin-maker/.gitignore deleted file mode 100644 index 796b96d1c..000000000 --- a/skin-maker/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/skin-maker/build.gradle b/skin-maker/build.gradle deleted file mode 100644 index e47a0fd42..000000000 --- a/skin-maker/build.gradle +++ /dev/null @@ -1,38 +0,0 @@ -apply plugin: 'com.android.library' - -version = QMUI_SKIN_MAKER_VERSION - -android { - compileSdkVersion parent.ext.compileSdkVersion - - - defaultConfig { - minSdkVersion parent.ext.minSdkVersion - targetSdkVersion parent.ext.targetSdkVersion - versionCode 1 - versionName "1.0" - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles 'consumer-rules.pro' - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - -} - -dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar']) - - api project(":qmui") - api project(":arch") - implementation "androidx.appcompat:appcompat:$appcompatVersion" - implementation "com.tencent:mmkv-static:$mmkvVersion" - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' -} diff --git a/skin-maker/src/androidTest/java/com/qmuiteam/qmui/skin/ExampleInstrumentedTest.java b/skin-maker/src/androidTest/java/com/qmuiteam/qmui/skin/ExampleInstrumentedTest.java deleted file mode 100644 index 462224ef2..000000000 --- a/skin-maker/src/androidTest/java/com/qmuiteam/qmui/skin/ExampleInstrumentedTest.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.Context; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; - -import static org.junit.Assert.assertEquals; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see <a href="http://d.android.com/tools/testing">Testing documentation</a> - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); - - assertEquals("com.qmuiteam.qmui.skin.test", appContext.getPackageName()); - } -} diff --git a/skin-maker/src/main/AndroidManifest.xml b/skin-maker/src/main/AndroidManifest.xml deleted file mode 100644 index 4cabd78e6..000000000 --- a/skin-maker/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ -<manifest package="com.qmuiteam.qmui.skin" - xmlns:android="http://schemas.android.com/apk/res/android" > - <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> -</manifest> diff --git a/skin-maker/src/main/java/com/qmuiteam/qmui/skin/DelegateViewPagerAdapter.java b/skin-maker/src/main/java/com/qmuiteam/qmui/skin/DelegateViewPagerAdapter.java deleted file mode 100644 index eafe2c03b..000000000 --- a/skin-maker/src/main/java/com/qmuiteam/qmui/skin/DelegateViewPagerAdapter.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.database.DataSetObserver; -import android.os.Parcelable; -import android.view.View; -import android.view.ViewGroup; - -import java.util.HashMap; - -import androidx.annotation.NonNull; -import androidx.viewpager.widget.PagerAdapter; - -public class DelegateViewPagerAdapter extends PagerAdapter { - private PagerAdapter origin; - private String prefix; - private HashMap<Object, Integer> mChildBindInfo = new HashMap<>(); - private QMUISkinMaker skinMaker; - - DelegateViewPagerAdapter(QMUISkinMaker skinMaker, PagerAdapter origin, String prefix) { - this.skinMaker = skinMaker; - this.origin = origin; - this.prefix = prefix; - } - - @Override - public int getCount() { - return origin.getCount(); - } - - @Override - public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { - return origin.isViewFromObject(view, object); - } - - @NonNull - @Override - public Object instantiateItem(@NonNull ViewGroup container, int position) { - Object item = origin.instantiateItem(container, position); - int id = skinMaker.bindViewPagerChild(origin, item, prefix); - mChildBindInfo.put(item, id); - return item; - } - - @Override - public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { - origin.destroyItem(container, position, object); - Integer bindId = mChildBindInfo.remove(object); - if (bindId != null) { - skinMaker.unBind(bindId); - } - } - - @Override - public void restoreState(Parcelable bundle, ClassLoader classLoader) { - origin.restoreState(bundle, classLoader); - } - - @Override - public Parcelable saveState() { - return origin.saveState(); - } - - @Override - public void startUpdate(@NonNull ViewGroup container) { - origin.startUpdate(container); - } - - @Override - public void finishUpdate(@NonNull ViewGroup container) { - origin.finishUpdate(container); - } - - @Override - public CharSequence getPageTitle(int position) { - return origin.getPageTitle(position); - } - - @Override - public float getPageWidth(int position) { - return origin.getPageWidth(position); - } - - @Override - public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) { - origin.setPrimaryItem(container, position, object); - } - - @Override - public void unregisterDataSetObserver(@NonNull DataSetObserver observer) { - origin.unregisterDataSetObserver(observer); - } - - @Override - public void registerDataSetObserver(@NonNull DataSetObserver observer) { - origin.registerDataSetObserver(observer); - } - - @Override - public void notifyDataSetChanged() { - super.notifyDataSetChanged(); - origin.notifyDataSetChanged(); - } - - @Override - public int getItemPosition(@NonNull Object object) { - return origin.getItemPosition(object); - } -} \ No newline at end of file diff --git a/skin-maker/src/main/java/com/qmuiteam/qmui/skin/QMUISkinMaker.java b/skin-maker/src/main/java/com/qmuiteam/qmui/skin/QMUISkinMaker.java deleted file mode 100644 index d039a170f..000000000 --- a/skin-maker/src/main/java/com/qmuiteam/qmui/skin/QMUISkinMaker.java +++ /dev/null @@ -1,930 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.content.Context; -import android.content.res.Resources; -import android.os.AsyncTask; -import android.os.Bundle; -import android.os.Environment; -import android.util.SparseArray; -import android.view.View; -import android.view.ViewGroup; -import android.webkit.WebView; -import android.widget.AbsListView; -import android.widget.AdapterView; -import android.widget.ImageView; -import android.widget.TextView; -import android.widget.Toast; - -import com.qmuiteam.qmui.layout.IQMUILayout; -import com.qmuiteam.qmui.qqface.QMUIQQFaceView; -import com.qmuiteam.qmui.util.QMUIDisplayHelper; -import com.qmuiteam.qmui.util.QMUIViewHelper; -import com.qmuiteam.qmui.widget.QMUILoadingView; -import com.qmuiteam.qmui.widget.QMUIProgressBar; -import com.qmuiteam.qmui.widget.QMUIRadiusImageView; -import com.qmuiteam.qmui.widget.QMUITopBar; -import com.qmuiteam.qmui.widget.QMUITopBarLayout; -import com.qmuiteam.qmui.widget.dialog.QMUITipDialog; -import com.qmuiteam.qmui.widget.grouplist.QMUIGroupListView; -import com.qmuiteam.qmui.widget.popup.QMUIPopups; -import com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton; -import com.qmuiteam.qmui.widget.tab.QMUITabSegment; -import com.tencent.mmkv.MMKV; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.lang.ref.WeakReference; -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Queue; -import java.util.Set; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.view.TintableBackgroundView; -import androidx.fragment.app.Fragment; -import androidx.recyclerview.widget.RecyclerView; -import androidx.viewpager.widget.PagerAdapter; -import androidx.viewpager.widget.ViewPager; - -/** - * encode: prefix:methodName@fieldName classFullName;codeValueInfo - * prefix: r-ref,m-method,i-id,a-adapter,c-createViewHolder, p-pagerAdapter.instantiateItem - * methodName: generated methodName to wrap generate code - */ -public class QMUISkinMaker { - private static final String MMKV_ID = "qmui_skin_maker"; - private static QMUISkinMaker sSkinMaker; - private int sNextId = 1; - private SparseArray<HashMap<View, ViewInfo>> mBindInfo = new SparseArray<>(); - private String[] mPackageNames; - private List<String> mAttrsInR; - private static Set<Class<? extends View>> mFilterViews = new HashSet<>(); - - public static boolean isInited() { - return sSkinMaker != null; - } - - public static QMUISkinMaker init(Context context, String[] packageNames, String[] attrPrexfixes, Class<?> attrsClassInR) { - if (sSkinMaker == null) { - sSkinMaker = new QMUISkinMaker(); - } - MMKV.initialize(context); - sSkinMaker.mPackageNames = packageNames; - - - Field[] fields = attrsClassInR.getFields(); - - sSkinMaker.mAttrsInR = new ArrayList<>(); - for (Field field : fields) { - String name = field.getName(); - if (name.startsWith("qmui_skin_support_")) { - sSkinMaker.mAttrsInR.add(name); - } else { - for (String prefix : attrPrexfixes) { - if (name.startsWith(prefix)) { - sSkinMaker.mAttrsInR.add(name); - break; - } - } - } - } - return sSkinMaker; - } - - public static QMUISkinMaker getInstance() { - if (sSkinMaker == null) { - throw new RuntimeException("must invoke init() to init sSkinMaker"); - } - return sSkinMaker; - } - - private QMUISkinMaker() { - } - - public static void addFilterView(Class<? extends View> view) { - mFilterViews.add(view); - } - - /** - * must be called after {@link Activity#setContentView(View)} - * - * @param activity - * @return - */ - public int bind(Activity activity) { - int ret = bindView(QMUIViewHelper.getActivityRoot(activity)); - HashMap<View, ViewInfo> viewInfoMap = mBindInfo.get(ret); - bindField(activity, activity.getClass().getSimpleName(), viewInfoMap); - restoreSkinFromMMKV(viewInfoMap); - handleForAdapter(viewInfoMap); - return ret; - } - - /** - * must be called in or after {@link Fragment#onViewCreated(View, Bundle)}} - * - * @param fragment - * @return - */ - public int bind(Fragment fragment) { - int ret = bindView(fragment.getView()); - HashMap<View, ViewInfo> viewInfoMap = mBindInfo.get(ret); - bindField(fragment, fragment.getClass().getSimpleName(), viewInfoMap); - restoreSkinFromMMKV(viewInfoMap); - handleForAdapter(viewInfoMap); - return ret; - } - - private int bindViewHolder(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, String prefix) { - int ret = bindView(viewHolder.itemView); - HashMap<View, ViewInfo> viewInfoMap = mBindInfo.get(ret); - String fullPrefix = prefix + "_" + viewHolder.itemView.getClass().getSimpleName(); - bindField(viewHolder, fullPrefix, viewInfoMap); - restoreSkinFromMMKV(viewInfoMap); - handleForAdapter(viewInfoMap); - RecyclerView.Adapter adapter = recyclerView.getAdapter(); - if (adapter != null) { - MMKV.mmkvWithID(MMKV_ID).encode("c:" + viewHolder.getClass().getName() + "@" + prefix, - adapter.getClass().getName() + ";" + fullPrefix); - } - return ret; - } - - int bindViewPagerChild(PagerAdapter pagerAdapter, Object item, String prefix) { - if (item instanceof Fragment) { - return -1; - } - if (item instanceof View) { - View itemView = (View) item; - int ret = bindView(itemView); - HashMap<View, ViewInfo> viewInfoMap = mBindInfo.get(ret); - String fullPrefix = prefix + "_" + itemView.getClass().getSimpleName(); - bindField(itemView, fullPrefix, viewInfoMap); - restoreSkinFromMMKV(viewInfoMap); - handleForAdapter(viewInfoMap); - MMKV.mmkvWithID(MMKV_ID).encode("p:" + prefix + "@" + itemView.getClass().getName(), - pagerAdapter.getClass().getName() + ";" + fullPrefix); - } - return -1; - } - - private void bindField(Object object, String prefix, HashMap<View, ViewInfo> viewInfoMap) { - Set<View> unIdentifyViews = new HashSet<>(viewInfoMap.keySet()); - // avoid circular reference - Set<Object> scannedObjects = new HashSet<>(100); - FieldNode tree = new FieldNode(); - tree.fieldClassName = object.getClass().getName(); - tree.fieldName = prefix; - tree.node = object; - if (object instanceof View) { - ViewInfo viewInfo = viewInfoMap.get(object); - if (viewInfo != null) { - viewInfo.fieldNode = tree; - viewInfo.fieldName = "this"; - } - } - recursiveReflect(tree, viewInfoMap, unIdentifyViews, scannedObjects); - if (!unIdentifyViews.isEmpty()) { - for (View view : unIdentifyViews) { - int id = view.getId(); - if (id != View.NO_ID) { - try { - String idName = view.getResources().getResourceName(view.getId()); - if (idName != null && idName.length() > 0) { - idName = idName.replaceAll(":id/", ".R.id."); - ViewInfo viewInfo = viewInfoMap.get(view); - if (viewInfo != null) { - viewInfo.idName = idName; - viewInfo.fieldNode = tree; - } - } - } catch (Resources.NotFoundException ignore) { - } - } - } - } - } - - private void handleForAdapter(HashMap<View, ViewInfo> viewInfoMap) { - for (View view : viewInfoMap.keySet()) { - ViewInfo viewInfo = viewInfoMap.get(view); - if (viewInfo == null || viewInfo.fieldNode == null) { - continue; - } - - String fieldKey = viewInfo.fieldNode.getKey(); - String prefix = fieldKey + "_" + viewInfo.getSimpleFieldName(); - if (view instanceof RecyclerView) { - setAdapterTag(viewInfo, fieldKey, prefix); - RecyclerView recyclerView = (RecyclerView) view; - for (int i = 0; i < recyclerView.getChildCount(); i++) { - bindViewHolder(recyclerView, recyclerView.getChildViewHolder(recyclerView.getChildAt(i)), prefix); - } - recyclerView.addOnChildAttachStateChangeListener(mRecyclerViewChildAttachStateChangeListener); - } else if (view instanceof ViewPager) { - setAdapterTag(viewInfo, fieldKey, prefix); - ViewPager viewPager = (ViewPager) view; - PagerAdapter adapter = viewPager.getAdapter(); - if (adapter != null && !(adapter instanceof DelegateViewPagerAdapter)) { - viewPager.setAdapter(new DelegateViewPagerAdapter(this, adapter, prefix)); - } - } - } - } - - - private void setAdapterTag(ViewInfo viewInfo, String fieldKey, String prefix) { - viewInfo.view.setTag(R.id.qmui_skin_adapter, prefix); - MMKV.mmkvWithID(MMKV_ID).encode("a:" + fieldKey + "@" + viewInfo.getFullFieldNameWithPrefix(), - viewInfo.fieldNode.fieldClassName + ";" + prefix + viewInfo.getSuffix()); - } - - - private int bindView(View view) { - int id = sNextId++; - HashMap<View, ViewInfo> viewInfoMap = new HashMap<>(); - mBindInfo.put(id, viewInfoMap); - innerBind(view, viewInfoMap); - return id; - } - - private RecyclerView.OnChildAttachStateChangeListener mRecyclerViewChildAttachStateChangeListener = new RecyclerView.OnChildAttachStateChangeListener() { - - private HashMap<View, Integer> mChildBindInfo = new HashMap<>(); - - @Override - public void onChildViewAttachedToWindow(@NonNull View view) { - RecyclerView parent = (RecyclerView) view.getParent(); - String prefix = (String) parent.getTag(R.id.qmui_skin_adapter); - int bindId = bindViewHolder(parent, parent.getChildViewHolder(view), prefix); - mChildBindInfo.put(view, bindId); - } - - @Override - public void onChildViewDetachedFromWindow(@NonNull View view) { - Integer bindId = mChildBindInfo.remove(view); - if (bindId != null) { - unBind(bindId); - } - - } - }; - - private void innerBind(View view, HashMap<View, ViewInfo> viewInfoMap) { - if (view instanceof QMUITopBar || - view instanceof QMUITopBarLayout || - view instanceof QMUITabSegment || - view instanceof WebView) { - // for navigation, stop bind - return; - } - - for (Class<? extends View> cls : mFilterViews) { - if (cls.isAssignableFrom(view.getClass())) { - return; - } - } - viewInfoMap.put(view, generateViewInfo(view)); - if (view instanceof AbsListView || view instanceof ViewPager || view instanceof RecyclerView) { - return; - } - if (view instanceof ViewGroup) { - ViewGroup viewGroup = (ViewGroup) view; - for (int i = 0; i < viewGroup.getChildCount(); i++) { - innerBind(viewGroup.getChildAt(i), viewInfoMap); - } - } - } - - public void unBind(int id) { - HashMap<View, ViewInfo> viewInfoMap = mBindInfo.get(id); - unBind(viewInfoMap); - mBindInfo.remove(id); - } - - private void unBind(HashMap<View, ViewInfo> viewInfoMap) { - if (viewInfoMap != null) { - for (View view : viewInfoMap.keySet()) { - ViewInfo viewInfo = viewInfoMap.get(view); - if (viewInfo != null) { - view.setOnClickListener(viewInfo.originClickListener); - } - if (view instanceof RecyclerView) { - ((RecyclerView) view).removeOnChildAttachStateChangeListener(mRecyclerViewChildAttachStateChangeListener); - } - } - } - } - - public void unBindAll() { - for (int i = 0; i < mBindInfo.size(); i++) { - unBind(mBindInfo.valueAt(i)); - } - mBindInfo.clear(); - } - - private ViewInfo generateViewInfo(final View view) { - final ViewInfo viewInfo = new ViewInfo(); - viewInfo.view = view; - try { - Field listenerInfoFiled = View.class.getDeclaredField("mListenerInfo"); - listenerInfoFiled.setAccessible(true); - Object listenerInfo = listenerInfoFiled.get(view); - if (listenerInfo != null) { - Field onClickListenerField = listenerInfo.getClass().getDeclaredField("mOnClickListener"); - onClickListenerField.setAccessible(true); - viewInfo.originClickListener = (View.OnClickListener) onClickListenerField.get(listenerInfo); - } - } catch (Exception e) { - e.printStackTrace(); - } - View.OnClickListener skinOnClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - final Context context = v.getContext(); - if (viewInfo.fieldNode == null) { - Toast.makeText(context, - "No Id And No Reference, Can not set skin by skinMaker.", - Toast.LENGTH_LONG).show(); - return; - } - QMUIGroupListView groupListView = new QMUIGroupListView(context); - groupListView.setId(QMUIViewHelper.generateViewId()); - QMUIGroupListView.Section section = QMUIGroupListView.newSection(context); - addItem(groupListView, section, viewInfo, "Background", new ValueUpdater() { - @Override - public void update(QMUISkinValueBuilder builder, String attrName) { - builder.background(attrName); - } - }); - - if (view instanceof TintableBackgroundView) { - addItem(groupListView, section, viewInfo, "BackgroundTintColor", new ValueUpdater() { - @Override - public void update(QMUISkinValueBuilder builder, String attrName) { - builder.bgTintColor(attrName); - } - }); - } - - addItem(groupListView, section, viewInfo, "Alpha", new ValueUpdater() { - @Override - public void update(QMUISkinValueBuilder builder, String attrName) { - builder.alpha(attrName); - } - }); - - if (view instanceof IQMUILayout) { - IQMUILayout layout = (IQMUILayout) view; - if (layout.hasTopSeparator()) { - addItem(groupListView, section, viewInfo, "TopSeparator", new ValueUpdater() { - @Override - public void update(QMUISkinValueBuilder builder, String attrName) { - builder.topSeparator(attrName); - } - }); - } - if (layout.hasRightSeparator()) { - addItem(groupListView, section, viewInfo, "RightSeparator", new ValueUpdater() { - @Override - public void update(QMUISkinValueBuilder builder, String attrName) { - builder.rightSeparator(attrName); - } - }); - } - - if (layout.hasBottomSeparator()) { - addItem(groupListView, section, viewInfo, "BottomSeparator", new ValueUpdater() { - @Override - public void update(QMUISkinValueBuilder builder, String attrName) { - builder.bottomSeparator(attrName); - } - }); - } - - if (layout.hasLeftSeparator()) { - addItem(groupListView, section, viewInfo, "LeftSeparator", new ValueUpdater() { - @Override - public void update(QMUISkinValueBuilder builder, String attrName) { - builder.leftSeparator(attrName); - } - }); - } - - if (layout.hasBorder()) { - addItem(groupListView, section, viewInfo, "Border", new ValueUpdater() { - @Override - public void update(QMUISkinValueBuilder builder, String attrName) { - builder.border(attrName); - } - }); - } - } - - if (view instanceof QMUIRadiusImageView) { - if (((QMUIRadiusImageView) view).getBorderWidth() > 0) { - addItem(groupListView, section, viewInfo, "Border", new ValueUpdater() { - @Override - public void update(QMUISkinValueBuilder builder, String attrName) { - builder.border(attrName); - } - }); - } - } - - if (view instanceof QMUIRoundButton) { - if (((QMUIRoundButton) view).getStrokeWidth() > 0) { - addItem(groupListView, section, viewInfo, "Border", new ValueUpdater() { - @Override - public void update(QMUISkinValueBuilder builder, String attrName) { - builder.border(attrName); - } - }); - } - } - - if (view instanceof QMUIProgressBar) { - addItem(groupListView, section, viewInfo, "TextColor", new ValueUpdater() { - @Override - public void update(QMUISkinValueBuilder builder, String attrName) { - builder.textColor(attrName); - } - }); - - addItem(groupListView, section, viewInfo, "ProgressColor", new ValueUpdater() { - @Override - public void update(QMUISkinValueBuilder builder, String attrName) { - builder.progressColor(attrName); - } - }); - } - - if (view instanceof ImageView) { - addItem(groupListView, section, viewInfo, "Src", new ValueUpdater() { - @Override - public void update(QMUISkinValueBuilder builder, String attrName) { - builder.src(attrName); - } - }); - - addItem(groupListView, section, viewInfo, "TintColor", new ValueUpdater() { - @Override - public void update(QMUISkinValueBuilder builder, String attrName) { - builder.tintColor(attrName); - } - }); - } - - if (view instanceof QMUILoadingView) { - addItem(groupListView, section, viewInfo, "TintColor", new ValueUpdater() { - @Override - public void update(QMUISkinValueBuilder builder, String attrName) { - builder.tintColor(attrName); - } - }); - } - - if (view instanceof TextView || view instanceof QMUIQQFaceView) { - addItem(groupListView, section, viewInfo, "TextColor", new ValueUpdater() { - @Override - public void update(QMUISkinValueBuilder builder, String attrName) { - builder.textColor(attrName); - } - }); - } - - section.setUseTitleViewForSectionSpace(false) - .setShowSeparator(false) - .addTo(groupListView); - QMUIPopups.popup(view.getContext(), QMUIDisplayHelper.dp2px(context, 200)) - .arrow(true) - .shadow(true) - .view(groupListView) - .dismissIfOutsideTouch(false) - .show(v); - } - }; - viewInfo.skinClickListener = skinOnClickListener; - if(!(view instanceof AdapterView)){ - view.setOnClickListener(skinOnClickListener); - } - - return viewInfo; - } - - private void addItem(QMUIGroupListView listView, - QMUIGroupListView.Section section, - final ViewInfo viewInfo, - CharSequence name, - final ValueUpdater valueUpdater) { - section.addItemView(listView.createItemView(name), new View.OnClickListener() { - @Override - public void onClick(View v) { - chooseAttr(viewInfo.view, new ValueWriter() { - @Override - public void write(String attrName) { - valueUpdater.update(viewInfo.valueBuilder, attrName); - QMUISkinHelper.setSkinValue(viewInfo.view, viewInfo.valueBuilder); - viewInfo.saveToMMKV(); - } - }); - } - }); - } - - - interface ValueUpdater { - void update(QMUISkinValueBuilder builder, String attrName); - } - - private void chooseAttr(View anchorView, ValueWriter valueWriter) { - SkinAttrChooseMakerPopup popup = new SkinAttrChooseMakerPopup(anchorView.getContext(), mAttrsInR, valueWriter); - popup.skinManager(QMUISkinManager.defaultInstance(anchorView.getContext())); - popup.show(anchorView); - } - - private static boolean doNotNeedToGetField(Class<?> cls) { - return cls.isPrimitive() || - cls.getName().startsWith("java"); - } - - private void recursiveReflect(FieldNode tree, HashMap<View, ViewInfo> viewInfoMap, - Set<View> unIdentifiedViews, - Set<Object> scannedObjects) { - - // BFS - Queue<FieldNode> queue = new LinkedList<>(); - queue.add(tree); - - while (true) { - if (unIdentifiedViews.isEmpty()) { - break; - } - FieldNode fieldNode = queue.poll(); - if (fieldNode == null) { - break; - } - Object object = fieldNode.node; - if (scannedObjects.contains(object)) { - continue; - } - scannedObjects.add(object); - if (!isBusinessClass(fieldNode.fieldClassName)) { - continue; - } - - Collection<Field> fields = new ArrayList<>(Arrays.asList(object.getClass().getDeclaredFields())); - Class<?> superClass = object.getClass().getSuperclass(); - while (superClass != null && !doNotNeedToGetField(superClass)) { - Field[] list = superClass.getDeclaredFields(); - for (Field field : list) { - if ((field.getModifiers() & (Modifier.PUBLIC | Modifier.PROTECTED)) != 0) { - fields.add(field); - } - } - superClass = superClass.getSuperclass(); - } - try { - for (Field field : fields) { - field.setAccessible(true); - Object value = field.get(object); - if (value != null) { - if (scannedObjects.contains(value)) { - continue; - } - if (value instanceof View) { - if (viewInfoMap.containsKey(value)) { - ViewInfo viewInfo = viewInfoMap.get(value); - if (viewInfo != null) { - unIdentifiedViews.remove(value); - fieldNode.viewInfos.add(viewInfo); - viewInfo.fieldNode = fieldNode; - viewInfo.fieldName = field.getName(); - } - } - } - if (!doNotNeedToGetField(value.getClass())) { - FieldNode childNode = new FieldNode(); - childNode.fieldClassName = value.getClass().getName(); - childNode.fieldName = field.getName(); - childNode.parent = fieldNode; - childNode.node = value; - fieldNode.children.add(childNode); - queue.add(childNode); - } - } - } - } catch (IllegalAccessException e) { - e.printStackTrace(); - } - } - alignTree(tree); - } - - private void alignTree(FieldNode node) { - if (!node.children.isEmpty()) { - for (int i = node.children.size() - 1; i >= 0; i--) { - FieldNode child = node.children.get(i); - alignTree(child); - if (child.children.isEmpty() && child.viewInfos.isEmpty()) { - node.children.remove(child); - } - } - } - } - - - private void restoreSkinFromMMKV(HashMap<View, ViewInfo> viewInfoMap) { - for (View view : viewInfoMap.keySet()) { - ViewInfo viewInfo = viewInfoMap.get(view); - FieldNode fieldNode = viewInfo.fieldNode; - if (fieldNode == null) { - continue; - } - String mmkvKey = viewInfo.getKey(); - String result = MMKV.mmkvWithID(MMKV_ID).decodeString(mmkvKey); - if (result == null || result.isEmpty()) { - continue; - } - String[] splits = result.split(";"); - if (fieldNode.fieldClassName.equals(splits[0])) { - String value = splits[1].replaceAll("@.*", ""); - QMUISkinHelper.setSkinValue(view, value); - viewInfo.valueBuilder.convertFrom(value); - } - } - } - - private boolean isBusinessClass(String className) { - for (String packageName : mPackageNames) { - if (className.startsWith(packageName)) { - return true; - } - } - return false; - } - - static class FieldNode { - String fieldClassName; - String fieldName; - Object node; - List<ViewInfo> viewInfos = new ArrayList<>(); - List<FieldNode> children = new ArrayList<>(); - FieldNode parent = null; - - @Nullable - public String getKey() { - StringBuilder key = new StringBuilder(); - key.append(fieldName); - FieldNode p = parent; - while (p != null) { - key.insert(0, "_"); - key.insert(0, parent.fieldName); - p = p.parent; - } - return key.toString(); - } - } - - - class ViewInfo { - public View.OnClickListener originClickListener; - public View.OnClickListener skinClickListener; - public View view; - public QMUISkinValueBuilder valueBuilder = QMUISkinValueBuilder.acquire(); - public FieldNode fieldNode; - public String fieldName; - public String idName; - - @Nullable - public String getKey() { - if (fieldNode == null) { - return null; - } - if (fieldName != null) { - return "r:" + fieldNode.getKey() + "@" + fieldName; - } else { - return "i:" + fieldNode.getKey() + "@" + idName; - } - } - - @Nullable - public String getSimpleFieldName() { - if (fieldName != null) { - return fieldName; - } - - if (idName != null) { - int index = idName.lastIndexOf("."); - if (index >= 0 && index < idName.length()) { - return idName.substring(index + 1); - } - } - return null; - } - - @Nullable - public String getFullFieldNameWithPrefix() { - if (fieldName != null) { - return fieldName; - } - - if (idName != null) { - return "i/" + idName; - } - return null; - } - - private String getSuffix() { - if (idName != null) { - if (fieldNode.node instanceof Activity || fieldNode.node instanceof ViewGroup) { - return "@this"; - } else if (fieldNode.node instanceof Fragment) { - return "@getView()"; - } else if (fieldNode.node instanceof RecyclerView.ViewHolder) { - return "@itemView"; - } - } - return ""; - } - - public void saveToMMKV() { - if (fieldNode == null) { - return; - } - StringBuilder builder = new StringBuilder(); - builder.append(fieldNode.fieldClassName); - builder.append(";"); - builder.append(valueBuilder.build()); - builder.append(getSuffix()); - - MMKV.mmkvWithID(MMKV_ID).encode(getKey(), builder.toString()); - FieldNode parent = fieldNode.parent; - FieldNode current = fieldNode; - while (parent != null) { - String baseKey = parent.getKey(); - builder.setLength(0); - builder.append(parent.fieldClassName); - builder.append(";"); - builder.append(baseKey + "_" + current.fieldName); - MMKV.mmkvWithID(MMKV_ID).encode("m:" + baseKey + "@" + current.fieldName, builder.toString()); - current = parent; - parent = parent.parent; - } - } - } - - public void export(Activity activity) { - new ExportTask(activity).execute(); - } - - interface ValueWriter { - void write(String attrNaME); - } - - class ExportTask extends AsyncTask<Void, Integer, Boolean> { - - private WeakReference<Activity> mActivityWeakReference; - private QMUITipDialog mQMUITipDialog; - - ExportTask(Activity activity) { - mActivityWeakReference = new WeakReference<>(activity); - } - - @Override - protected void onPreExecute() { - Activity activity = mActivityWeakReference.get(); - if (activity != null) { - mQMUITipDialog = new QMUITipDialog.Builder(activity) - .setIconType(QMUITipDialog.Builder.ICON_TYPE_LOADING) - .setTipWord("exporting") - .create(); - mQMUITipDialog.show(); - } - } - - @Override - protected Boolean doInBackground(Void... voids) { - Activity activity = mActivityWeakReference.get(); - if (activity == null) { - return false; - } - MMKV kv = MMKV.mmkvWithID(MMKV_ID); - String[] keys = kv.allKeys(); - if(keys == null || keys.length == 0){ - return false; - } - HashMap<String, ArrayList<String>> result = new HashMap<>(); - for (String key : keys) { - String value = kv.decodeString(key); - String[] vv = value.split(";"); - String className = vv[0]; - ArrayList<String> code = result.get(className); - if (code == null) { - code = new ArrayList<>(); - result.put(className, code); - } - - int fieldIndex = key.lastIndexOf("@"); - String type = null; - String methodName = key.substring(2, fieldIndex); - if (key.startsWith("m:")) { - type = "method"; - } else if (key.startsWith("r:")) { - type = "ref"; - } else if (key.startsWith("i:")) { - type = "id"; - } else if (key.startsWith("a:")) { - type = "adapter"; - } else if (key.startsWith("c:")) { - type = "c"; - } else if (key.startsWith("p:")) { - type = "p"; - } - - if (type != null) { - code.add(methodName + - "," + - type + - "," + - key.substring(fieldIndex + 1) + - "," + - vv[1]); - } - } - - File dir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "qmui-skin-maker"); - if (!dir.exists()) { - dir.mkdirs(); - } - DateFormat dateFormat = new SimpleDateFormat("yyyy.MM.dd_kk.mm.ss", Locale.getDefault()); - File file = new File(dir, "skin_" + dateFormat.format(new Date(System.currentTimeMillis())) + ".txt"); - try { - FileWriter fileWriter = new FileWriter(file); - for (String key : result.keySet()) { - List<String> values = result.get(key); - if (values != null && !values.isEmpty()) { - fileWriter.write(key); - fileWriter.write('\n'); - for (String v : values) { - fileWriter.write(v); - fileWriter.write('\n'); - } - fileWriter.write(";\n"); - } - } - fileWriter.flush(); - fileWriter.close(); - } catch (IOException e) { - e.printStackTrace(); - } - return true; - } - - @Override - protected void onPostExecute(Boolean success) { - if (mQMUITipDialog != null) { - mQMUITipDialog.dismiss(); - } - Activity activity = mActivityWeakReference.get(); - if (activity != null) { - if (success) { - Toast.makeText(activity, "export success", Toast.LENGTH_SHORT).show(); - } else { - Toast.makeText(activity, "export failed", Toast.LENGTH_SHORT).show(); - } - } - - } - } -} diff --git a/skin-maker/src/main/java/com/qmuiteam/qmui/skin/SkinAttrChooseMakerPopup.java b/skin-maker/src/main/java/com/qmuiteam/qmui/skin/SkinAttrChooseMakerPopup.java deleted file mode 100644 index cfaa2bece..000000000 --- a/skin-maker/src/main/java/com/qmuiteam/qmui/skin/SkinAttrChooseMakerPopup.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.Context; -import android.graphics.Color; -import android.view.View; -import android.view.ViewGroup; - -import com.qmuiteam.qmui.layout.QMUIButton; -import com.qmuiteam.qmui.qqface.QMUIQQFaceView; -import com.qmuiteam.qmui.util.QMUIDisplayHelper; -import com.qmuiteam.qmui.util.QMUIResHelper; -import com.qmuiteam.qmui.widget.popup.QMUIFullScreenPopup; - -import java.util.List; - -import androidx.annotation.NonNull; -import androidx.constraintlayout.widget.ConstraintLayout; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -class SkinAttrChooseMakerPopup extends QMUIFullScreenPopup { - private final QMUISkinMaker.ValueWriter mValueWriter; - private QMUIButton mAddNewAttrBtn; - private RecyclerView mRecyclerView; - private List<String> mAttrs; - - public SkinAttrChooseMakerPopup(Context context, List<String> attrs, QMUISkinMaker.ValueWriter valueWriter) { - super(context); - mValueWriter = valueWriter; - mAttrs = attrs; - closeBtn(true); - - int btnHeight = QMUIDisplayHelper.dp2px(context, 54); - mAddNewAttrBtn = new QMUIButton(context); - mAddNewAttrBtn.setText(R.string.app_new_attr); - mAddNewAttrBtn.setId(View.generateViewId()); - mAddNewAttrBtn.setRadius(btnHeight / 2); - mAddNewAttrBtn.setBackgroundColor(Color.WHITE); - mAddNewAttrBtn.setChangeAlphaWhenPress(true); - - int marginHor = QMUIDisplayHelper.dp2px(context, 24); - ConstraintLayout.LayoutParams newAttrLp = new ConstraintLayout.LayoutParams(0, btnHeight); - newAttrLp.leftMargin = marginHor; - newAttrLp.rightMargin = marginHor; - newAttrLp.topMargin = QMUIDisplayHelper.dp2px(context, 60); - newAttrLp.leftToLeft = ConstraintLayout.LayoutParams.PARENT_ID; - newAttrLp.rightToRight = ConstraintLayout.LayoutParams.PARENT_ID; - newAttrLp.topToTop = ConstraintLayout.LayoutParams.PARENT_ID; - addView(mAddNewAttrBtn, newAttrLp); - - mRecyclerView = new RecyclerView(context); - mRecyclerView.setId(View.generateViewId()); - mRecyclerView.setBackgroundColor(Color.WHITE); - mRecyclerView.setLayoutManager(new LinearLayoutManager(context)); - mRecyclerView.setAdapter(new Adapter()); - - ConstraintLayout.LayoutParams recyclerViewLp = new ConstraintLayout.LayoutParams( - 0, 0); - recyclerViewLp.leftMargin = marginHor; - recyclerViewLp.rightMargin = marginHor; - recyclerViewLp.topMargin = QMUIDisplayHelper.dp2px(context, 20); - recyclerViewLp.leftToLeft = ConstraintLayout.LayoutParams.PARENT_ID; - recyclerViewLp.rightToRight = ConstraintLayout.LayoutParams.PARENT_ID; - recyclerViewLp.topToBottom = mAddNewAttrBtn.getId(); - recyclerViewLp.bottomMargin = QMUIDisplayHelper.dp2px(context, 20); - recyclerViewLp.bottomToTop = getCloseBtnId(); - addView(mRecyclerView, recyclerViewLp); - } - - class VH extends RecyclerView.ViewHolder { - private QMUIQQFaceView mQMUIQQFaceView; - private String mAttrName; - - public VH(@NonNull QMUIQQFaceView itemView) { - super(itemView); - mQMUIQQFaceView = itemView; - mQMUIQQFaceView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (mAttrName != null) { - mValueWriter.write(mAttrName); - dismiss(); - } - } - }); - } - - public void bind(@NonNull String attr) { - mAttrName = attr; - mQMUIQQFaceView.setText(mAttrName); - } - } - - class Adapter extends RecyclerView.Adapter<VH> { - - @NonNull - @Override - public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - QMUIQQFaceView qmuiqqFaceView = new QMUIQQFaceView(parent.getContext()); - qmuiqqFaceView.setTextSize(QMUIDisplayHelper.sp2px(parent.getContext(), 15)); - qmuiqqFaceView.setTextColor(Color.BLACK); - int paddingHor = QMUIDisplayHelper.dp2px(parent.getContext(), 20); - int paddingVer = QMUIDisplayHelper.dp2px(parent.getContext(), 12); - qmuiqqFaceView.setBackground(QMUIResHelper.getAttrDrawable( - parent.getContext(), R.attr.qmui_skin_support_s_list_item_bg_1)); - qmuiqqFaceView.setPadding(paddingHor, paddingVer, paddingHor, paddingVer); - return new VH(qmuiqqFaceView); - } - - @Override - public void onBindViewHolder(@NonNull VH holder, int position) { - holder.bind(mAttrs.get(position)); - } - - @Override - public int getItemCount() { - return mAttrs.size(); - } - } -} diff --git a/type/build.gradle b/type/build.gradle deleted file mode 100644 index 58ecffdce..000000000 --- a/type/build.gradle +++ /dev/null @@ -1,35 +0,0 @@ -apply plugin: 'com.android.library' - -version = QMUI_TYPE_VERSION - -android { - - compileSdkVersion parent.ext.compileSdkVersion - lintOptions { - abortOnError false - } - - defaultConfig { - minSdkVersion parent.ext.minSdkVersion - targetSdkVersion parent.ext.targetSdkVersion - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } - -} - -dependencies { - api "androidx.annotation:annotation:$annotationVersion" - api "androidx.core:core:$appcompatVersion" -} - -// deploy -File deployConfig = rootProject.file('gradle/deploy.properties') -if (deployConfig.exists()) { - apply from: rootProject.file('gradle/deploy.gradle') -} \ No newline at end of file diff --git a/type/build.gradle.kts b/type/build.gradle.kts new file mode 100644 index 000000000..748ce4d00 --- /dev/null +++ b/type/build.gradle.kts @@ -0,0 +1,42 @@ +import com.qmuiteam.plugin.Dep + +plugins { + id("com.android.library") + kotlin("android") + `maven-publish` + signing + id("qmui-publish") +} + +version = Dep.QMUI.typeVer + + +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 { + implementation(Dep.AndroidX.appcompat) + implementation(Dep.AndroidX.annotation) + implementation(Dep.AndroidX.coreKtx) +} \ No newline at end of file diff --git a/type/src/main/java/com/qmuiteam/qmui/type/EnvironmentUpdater.java b/type/src/main/java/com/qmuiteam/qmui/type/EnvironmentUpdater.kt similarity index 86% rename from type/src/main/java/com/qmuiteam/qmui/type/EnvironmentUpdater.java rename to type/src/main/java/com/qmuiteam/qmui/type/EnvironmentUpdater.kt index ed3fd0be1..997fb9bdf 100644 --- a/type/src/main/java/com/qmuiteam/qmui/type/EnvironmentUpdater.java +++ b/type/src/main/java/com/qmuiteam/qmui/type/EnvironmentUpdater.kt @@ -13,9 +13,8 @@ * either express or implied. See the License for the specific language governing permissions and * limitations under the License. */ +package com.qmuiteam.qmui.type -package com.qmuiteam.qmui.type; - -public interface EnvironmentUpdater { - void update(TypeEnvironment env); -} +fun interface EnvironmentUpdater { + fun update(env: TypeEnvironment) +} \ No newline at end of file diff --git a/type/src/main/java/com/qmuiteam/qmui/type/Line.java b/type/src/main/java/com/qmuiteam/qmui/type/Line.java deleted file mode 100644 index 82050dc2a..000000000 --- a/type/src/main/java/com/qmuiteam/qmui/type/Line.java +++ /dev/null @@ -1,297 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, - * either express or implied. See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.qmuiteam.qmui.type; - -import android.graphics.Canvas; - -import androidx.core.util.Pools; - -import com.qmuiteam.qmui.type.element.BreakWordLineElement; -import com.qmuiteam.qmui.type.element.CharOrPhraseElement; -import com.qmuiteam.qmui.type.element.Element; -import com.qmuiteam.qmui.type.element.NextParagraphElement; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; - -public class Line { - private static Pools.Pool<Line> sLinePool = new Pools.SimplePool<>(16); - - public static Line acquire() { - Line line = sLinePool.acquire(); - if (line == null) { - line = new Line(); - } - return line; - } - - private Line() { - - } - - private int mX; - private int mY; - private int mWidthLimit; - private int mContentWidth; - private int mContentHeight; - private int mLayoutWidth; - private List<Element> mElements = new LinkedList<>(); - private HashMap<Element, Integer> mVisibleChanged; - - public void init(int x, int y, int widthLimit) { - mX = x; - mY = y; - mWidthLimit = widthLimit; - } - - public void add(Element element) { - mElements.add(element); - mContentWidth += element.getMeasureWidth(); - mContentHeight = (int) Math.max(mContentHeight, element.getMeasureHeight()); - } - - public void addFirst(Element element){ - mElements.add(0, element); - mContentWidth += element.getMeasureWidth(); - mContentHeight = (int) Math.max(mContentHeight, element.getMeasureHeight()); - } - - public Element first(){ - return mElements.isEmpty() ? null : mElements.get(0); - } - - public int getSize() { - return mElements.size(); - } - - public int getContentWidth() { - return mContentWidth; - } - - public int getLayoutWidth() { - return mLayoutWidth; - } - - public int getWidthLimit() { - return mWidthLimit; - } - - public int getContentHeight() { - return mContentHeight; - } - - public int getX() { - return mX; - } - - public int getY() { - return mY; - } - - public void setX(int x) { - mX = x; - } - - public void setY(int y) { - mY = y; - } - - void move(TypeEnvironment environment){ - for(Element el: mElements){ - el.move(environment); - } - } - - public List<Element> handleWordBreak(TypeEnvironment environment) { - if (mElements.size() == 0) { - return null; - } - int lastIndex = mElements.size() - 1; - Element last = mElements.get(lastIndex); - if (last.getWordPart() == Element.WORD_PART_WHOLE || last.hasEnvironmentUpdater()) { - return null; - } - List<Element> back = new LinkedList<>(); - back.add(last); - mElements.remove(lastIndex); - lastIndex--; - int min = Math.max(0, lastIndex - 20); // try 20 letter. - boolean find = false; - while (lastIndex > min) { - Element el = mElements.get(lastIndex); - - if (el.getWordPart() == Element.WORD_PART_WHOLE) { - find = true; - break; - } else if (el.isCanBreakWord()) { - BreakWordLineElement b = new BreakWordLineElement(); - b.measure(environment); - add(b); - find = true; - break; - } else if (el.hasEnvironmentUpdater()) { - // give up - mElements.addAll(back); - return null; - } else { - back.add(0, el); - mElements.remove(lastIndex); - lastIndex--; - } - } - - if (!find) { - // give up - mElements.addAll(back); - return null; - } - - if (back.isEmpty()) { - return null; - } - - for (Element el : back) { - mContentWidth -= el.getMeasureWidth(); - } - return back; - } - - private boolean hideLastIfSpaceIfNeeded(boolean dropLastIfSpace) { - Element last = mElements.get(mElements.size() - 1); - if (dropLastIfSpace && last instanceof CharOrPhraseElement && last.getChar() == ' ' && last.getVisible() != Element.GONE) { - changeVisibleInner(last, Element.GONE); - mContentWidth -= last.getMeasureWidth(); - return true; - } - return false; - } - - private void changeVisibleInner(Element element, int visible) { - int oldVal = element.getVisible(); - if (visible == oldVal) { - return; - } - if (mVisibleChanged == null) { - mVisibleChanged = new HashMap<>(); - } - mVisibleChanged.put(element, oldVal); - element.setVisible(visible); - } - - private int calculateGapCount() { - int ret = 0; - for (int i = 1; i < mElements.size(); i++) { - Element el = mElements.get(i); - if (el.getVisible() != Element.GONE && - (el.getWordPart() == Element.WORD_PART_WHOLE || - el.getWordPart() == Element.WORD_PART_START)) { - ret++; - } - } - return ret; - } - - public boolean isMiddleParagraphEndLine(){ - return !mElements.isEmpty() && mElements.get(mElements.size() - 1) instanceof NextParagraphElement; - } - - public void layout(TypeEnvironment env, boolean dropLastIfSpace, boolean isEnd) { - if (mElements.isEmpty()) { - return; - } - hideLastIfSpaceIfNeeded(dropLastIfSpace); - mLayoutWidth = mContentWidth; - TypeEnvironment.Alignment alignment = env.getAlignment(); - float start = mX; - float addSpace = 0; - if (alignment == TypeEnvironment.Alignment.RIGHT) { - start = mX + mWidthLimit - mContentWidth; - } else if (alignment == TypeEnvironment.Alignment.CENTER) { - start = mX + (mWidthLimit - mContentWidth) / 2f; - } else if (alignment == TypeEnvironment.Alignment.JUSTIFY) { - float remain = mWidthLimit - mContentWidth; - if (!(isEnd || isMiddleParagraphEndLine()) || remain < env.getLastLineJustifyMaxWidth()) { - int gapCount = calculateGapCount(); - if (gapCount > 0) { - addSpace = remain / gapCount; - mLayoutWidth = mWidthLimit; - } - } - } - float x = start; - for (int i = 0; i < mElements.size(); i++) { - Element el = mElements.get(i); - if (i > 0 && (el.getWordPart() == Element.WORD_PART_WHOLE - || el.getWordPart() == Element.WORD_PART_START)) { - x += addSpace; - } - el.setX((int) x); - x += el.getMeasureWidth(); - el.setY(mY + (mContentHeight - el.getMeasureHeight()) / 2f); - } - } - - public void draw(TypeEnvironment env, Canvas canvas) { - for (Element element : mElements) { - element.draw(env, canvas); - } - } - - void restoreVisibleChange(){ - if (mVisibleChanged != null) { - for (Element entry : mVisibleChanged.keySet()) { - Integer visible = mVisibleChanged.get(entry); - if (visible != null) { - entry.setVisible(visible); - } - } - mVisibleChanged.clear(); - } - } - - public List<Element> popAll(){ - List<Element> elements = new ArrayList<>(mElements); - mElements.clear(); - restoreVisibleChange(); - mContentWidth = 0; - mContentHeight = 0; - mLayoutWidth = 0; - return elements; - } - - public void clear(){ - mElements.clear(); - restoreVisibleChange(); - mContentWidth = 0; - mContentHeight = 0; - mLayoutWidth = 0; - } - - public void release() { - mX = 0; - mY = 0; - mWidthLimit = 0; - mContentWidth = 0; - mContentHeight = 0; - mLayoutWidth = 0; - mElements.clear(); - restoreVisibleChange(); - sLinePool.release(this); - } -} diff --git a/type/src/main/java/com/qmuiteam/qmui/type/Line.kt b/type/src/main/java/com/qmuiteam/qmui/type/Line.kt new file mode 100644 index 000000000..991416e2d --- /dev/null +++ b/type/src/main/java/com/qmuiteam/qmui/type/Line.kt @@ -0,0 +1,278 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.qmuiteam.qmui.type + +import android.graphics.Canvas +import androidx.core.util.Pools +import com.qmuiteam.qmui.type.element.BreakWordLineElement +import com.qmuiteam.qmui.type.element.Element +import com.qmuiteam.qmui.type.element.NextParagraphElement +import com.qmuiteam.qmui.type.element.TextElement +import java.util.* + +class Line private constructor() { + companion object { + private val sLinePool: Pools.Pool<Line> = Pools.SimplePool(16) + + fun acquire(): Line { + var line = sLinePool.acquire() + if (line == null) { + line = Line() + } + return line + } + } + + var x = 0 + var y = 0 + var widthLimit = 0 + private set + var contentWidth = 0 + private set + var contentHeight = 0 + private set + var layoutWidth = 0 + private set + private val mElements = LinkedList<Element>() + private var mVisibleChanged: HashMap<Element, Int>? = null + + + val size: Int + get() = mElements.size + + fun get(i: Int): Element? { + return mElements.getOrNull(i) + } + + fun init(x: Int, y: Int, widthLimit: Int) { + this.x = x + this.y = y + this.widthLimit = widthLimit + } + + fun add(element: Element) { + mElements.add(element) + contentWidth += element.measureWidth + contentHeight = contentHeight.coerceAtLeast(element.measureHeight) + } + + fun addFirst(element: Element) { + mElements.add(0, element) + contentWidth += element.measureWidth + contentHeight = contentHeight.coerceAtLeast(element.measureHeight) + } + + fun first(): Element? { + return if (mElements.isEmpty()) null else mElements[0] + } + + fun move(environment: TypeEnvironment?) { + for (el in mElements) { + el.move(environment!!) + } + } + + fun handleWordBreak(environment: TypeEnvironment, shouldHandleWordBreak: Boolean): List<Element>? { + if (mElements.size == 0 || !shouldHandleWordBreak) { + return null + } + var lastIndex = mElements.size - 1 + val last = mElements[lastIndex] + val next = last.next + val back: MutableList<Element> = LinkedList() + if (last.wordPart == Element.WORD_PART_WHOLE) { + if (last.lineBreakType == Element.LINE_BREAK_TYPE_NOT_END || + next != null && next.lineBreakType == Element.LINE_BREAK_TYPE_NOT_START + ) { + mElements.removeAt(lastIndex) + back.add(last) + } + } else if (last.wordPart == Element.WORD_PART_END && next != null && next.lineBreakType != Element.LINE_BREAK_TYPE_NOT_START) { + // do nothing + } else if (last.wordPart == Element.WORD_PART_START) { + mElements.removeAt(lastIndex) + back.add(last) + } else { + back.add(last) + mElements.removeAt(lastIndex) + lastIndex-- + val min = 0.coerceAtLeast(lastIndex - environment.workBreakMaxTryLength) + var find = false + while (lastIndex > min) { + val el = mElements[lastIndex] + if (el.wordPart == Element.WORD_PART_WHOLE || el.wordPart == Element.WORD_PART_END) { + find = true + break + } else if (el.lineBreakType == Element.LINE_BREAK_WORD_BREAK_ALLOWED) { + // TODO what if environment had changed after break? the measure may be wrong + val b = BreakWordLineElement() + b.measure(environment) + val backWidth = back.sumOf { it.measureWidth } + if (backWidth >= b.measureWidth) { + find = true + add(b) + break + } else { + back.add(0, el) + mElements.removeAt(lastIndex) + lastIndex-- + } + } else { + back.add(0, el) + mElements.removeAt(lastIndex) + lastIndex-- + } + } + if (!find) { + // give up + mElements.addAll(back) + return null + } + } + if (back.isEmpty()) { + return null + } + for (el in back) { + contentWidth -= el.measureWidth + } + return back + } + + private fun hideLastIfSpaceIfNeeded(dropLastIfSpace: Boolean): Boolean { + val last = mElements[mElements.size - 1] + if (dropLastIfSpace && last is TextElement && last.length == 1 && last.text[0] == ' ' && last.visible != Element.GONE) { + changeVisibleInner(last, Element.GONE) + contentWidth -= last.measureWidth + return true + } + return false + } + + private fun changeVisibleInner(element: Element, visible: Int) { + val oldVal = element.visible + if (visible == oldVal) { + return + } + if (mVisibleChanged == null) { + mVisibleChanged = HashMap() + } + mVisibleChanged!![element] = oldVal + element.visible = visible + } + + private fun calculateGapCount(): Int { + var ret = 0 + for (i in 1 until mElements.size) { + val el = mElements[i] + if (el.visible != Element.GONE && + (el.wordPart == Element.WORD_PART_WHOLE || + el.wordPart == Element.WORD_PART_START) + ) { + ret++ + } + } + return ret + } + + val isMiddleParagraphEndLine: Boolean + get() = !mElements.isEmpty() && mElements[mElements.size - 1] is NextParagraphElement + + fun layout(env: TypeEnvironment, dropLastIfSpace: Boolean, isEnd: Boolean) { + if (mElements.isEmpty()) { + return + } + hideLastIfSpaceIfNeeded(dropLastIfSpace) + layoutWidth = contentWidth + val alignment = env.alignment + var start = x + var addSpace = 0 + if (alignment === TypeEnvironment.Alignment.RIGHT) { + start = x + widthLimit - contentWidth + } else if (alignment === TypeEnvironment.Alignment.CENTER) { + start = x + (widthLimit - contentWidth) / 2 + } else if (alignment === TypeEnvironment.Alignment.JUSTIFY) { + val remain = widthLimit - contentWidth + if (!(isEnd || isMiddleParagraphEndLine) || remain < env.lastLineJustifyMaxWidth) { + val gapCount = calculateGapCount() + if (gapCount > 0) { + addSpace = remain / gapCount + layoutWidth = widthLimit + } + } + } + var x = start + for (i in mElements.indices) { + val el = mElements[i] + if (i > 0 && (el.wordPart == Element.WORD_PART_WHOLE + || el.wordPart == Element.WORD_PART_START) + ) { + x += addSpace + mElements[i - 1].nextGapWidth = addSpace + } + el.x = x + x += el.measureWidth + el.y = y + (contentHeight - el.measureHeight) / 2 + } + } + + fun draw(env: TypeEnvironment, canvas: Canvas) { + for (element in mElements) { + element.draw(env, canvas) + } + } + + fun restoreVisibleChange() { + if (mVisibleChanged != null) { + for (entry in mVisibleChanged!!.keys) { + val visible = mVisibleChanged!![entry] + if (visible != null) { + entry.visible = visible + } + } + mVisibleChanged!!.clear() + } + } + + fun popAll(): List<Element> { + val elements: List<Element> = ArrayList(mElements) + mElements.clear() + restoreVisibleChange() + contentWidth = 0 + contentHeight = 0 + layoutWidth = 0 + return elements + } + + fun clear() { + mElements.clear() + restoreVisibleChange() + contentWidth = 0 + contentHeight = 0 + layoutWidth = 0 + } + + fun release() { + x = 0 + y = 0 + widthLimit = 0 + contentWidth = 0 + contentHeight = 0 + layoutWidth = 0 + mElements.clear() + restoreVisibleChange() + sLinePool.release(this) + } +} \ No newline at end of file diff --git a/type/src/main/java/com/qmuiteam/qmui/type/LineIndentHandler.kt b/type/src/main/java/com/qmuiteam/qmui/type/LineIndentHandler.kt new file mode 100644 index 000000000..90829e52f --- /dev/null +++ b/type/src/main/java/com/qmuiteam/qmui/type/LineIndentHandler.kt @@ -0,0 +1,59 @@ +package com.qmuiteam.qmui.type + +import com.qmuiteam.qmui.type.element.Element + +interface LineIndentHandler { + fun reset() + fun processIndent(typeModel: TypeModel, firstElement: Element, newParagraph: Boolean): Int +} + +class SerialLineIndentHandler( + serials: List<Pair<Int, Int>>, + private val followIndentForNewParagraphIfNeeded: Boolean = false): LineIndentHandler { + + private val sorted = serials.sortedBy { + it.first + } + + var currentIntend = 0 + private var pendingCalculatePair: Pair<Int, Int>? = null + var nextIndex = 0 + + override fun reset() { + currentIntend = 0 + nextIndex = 0 + } + + override fun processIndent(typeModel: TypeModel, firstElement: Element, newParagraph: Boolean): Int { + if(newParagraph){ + val pair = sorted.find { it.first == firstElement.index } + if(pair != null){ + currentIntend = 0 + }else if(!followIndentForNewParagraphIfNeeded){ + currentIntend = 0 + } else { + // make sure it's calculated. + pendingCalculatePair?.let { + currentIntend = calculateIndent(typeModel, it) + } + } + pendingCalculatePair = pair + }else{ + pendingCalculatePair?.let { + currentIntend = calculateIndent(typeModel, it) + } + pendingCalculatePair = null + } + return currentIntend + } + + private fun calculateIndent(typeModel: TypeModel, pair: Pair<Int, Int>): Int { + var indent = 0 + var el = typeModel.getByPos(pair.first) + while (el != null && el.start <= pair.second){ + indent += el.measureWidth + el = el.next + } + return indent + } +} \ No newline at end of file diff --git a/type/src/main/java/com/qmuiteam/qmui/type/LineLayout.java b/type/src/main/java/com/qmuiteam/qmui/type/LineLayout.java deleted file mode 100644 index 8b8626f81..000000000 --- a/type/src/main/java/com/qmuiteam/qmui/type/LineLayout.java +++ /dev/null @@ -1,467 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, - * either express or implied. See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.qmuiteam.qmui.type; - -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Typeface; -import android.text.TextUtils; - -import com.qmuiteam.qmui.type.element.BreakWordLineElement; -import com.qmuiteam.qmui.type.element.CharOrPhraseElement; -import com.qmuiteam.qmui.type.element.EffectElement; -import com.qmuiteam.qmui.type.element.Element; -import com.qmuiteam.qmui.type.element.NextParagraphElement; - -import java.util.ArrayList; -import java.util.Deque; -import java.util.LinkedList; -import java.util.List; -import java.util.Queue; - -public class LineLayout { - private int mMaxLines = Integer.MAX_VALUE; - private TextUtils.TruncateAt mEllipsize; - private boolean mCalculateWholeLines = true; - private TypeEnvironment mTypeEnvironment; - private TypeModel mTypeModel; - private List<Line> mLines = new ArrayList<>(); - private boolean mDropLastIfSpace = true; - private String mMoreText = null; - private int mMoreTextColor = 0; - private Typeface mMoreTextTypeface = null; - private int mMoreUnderlineColor = Color.TRANSPARENT; - private int mMoreBgColor = 0; - private int mMoreUnderlineHeight = 0; - private int mTotalLineCount = 0; - - public LineLayout(TypeEnvironment environment) { - mTypeEnvironment = environment; - } - - public LineLayout setMaxLines(int maxLines) { - mMaxLines = maxLines; - return this; - } - - public LineLayout setEllipsize(TextUtils.TruncateAt ellipsize) { - mEllipsize = ellipsize; - return this; - } - - public LineLayout setCalculateWholeLines(boolean calculateWholeLines) { - mCalculateWholeLines = calculateWholeLines; - return this; - } - - public LineLayout setDropLastIfSpace(boolean dropLastIfSpace) { - mDropLastIfSpace = dropLastIfSpace; - return this; - } - - public LineLayout setMoreText(String text, int color, Typeface typeface) { - mMoreText = text; - mMoreTextColor = color; - mMoreTextTypeface = typeface; - return this; - } - - public LineLayout setMoreBackgroundColor(int color) { - mMoreBgColor = color; - return this; - } - - public LineLayout setUnderline(int height, int color) { - mMoreUnderlineHeight = height; - mMoreUnderlineColor = color; - return this; - } - - public LineLayout setTypeModel(TypeModel typeModel) { - mTypeModel = typeModel; - return this; - } - - public void measureAndLayout() { - mTypeEnvironment.clear(); - release(); - if (mTypeModel == null) { - return; - } - Element element = mTypeModel.firstElement(); - if (element == null) { - return; - } - Line line = Line.acquire(); - int y = 0; - line.init(0, y, mTypeEnvironment.getWidthLimit()); - while (element != null) { - element.measure(mTypeEnvironment); - if (element instanceof NextParagraphElement) { - line.add(element); - line.layout(mTypeEnvironment, mDropLastIfSpace, false); - mLines.add(line); - if (canInterrupt()) { - return; - } - y += line.getContentHeight() + mTypeEnvironment.getParagraphSpace(); - line = createNewLine(y); - } else if (line.getContentWidth() + element.getMeasureWidth() > mTypeEnvironment.getWidthLimit()) { - if (mLines.size() == 0 && line.getSize() == 0) { - // the width is too small. - line.release(); - return; - } - List<Element> back = line.handleWordBreak(mTypeEnvironment); - line.layout(mTypeEnvironment, mDropLastIfSpace, false); - mLines.add(line); - if (canInterrupt()) { - handleEllipse(true); - return; - } - y += line.getContentHeight() + mTypeEnvironment.getLineSpace(); - line = createNewLine(y); - if (back != null && !back.isEmpty()) { - for (Element el : back) { - line.add(el); - } - } - line.add(element); - } else { - line.add(element); - } - element = element.getNext(); - } - if (line.getSize() > 0) { - line.layout(mTypeEnvironment, mDropLastIfSpace, true); - mLines.add(line); - } else { - line.release(); - } - mTotalLineCount = mLines.size(); - handleEllipse(false); - } - - private Line createNewLine(int y) { - Line line = Line.acquire(); - line.init(0, y, mTypeEnvironment.getWidthLimit()); - return line; - } - - private void handleEllipse(boolean fromInterrupt) { - if (mLines.isEmpty() || mLines.size() < mMaxLines || (mLines.size() == mMaxLines && !fromInterrupt)) { - return; - } - - if (mEllipsize == TextUtils.TruncateAt.END) { - handleEllipseEnd(); - } else if (mEllipsize == TextUtils.TruncateAt.START) { - handleEllipseStart(); - } else if (mEllipsize == TextUtils.TruncateAt.MIDDLE) { - handleEllipseMiddle(); - } - } - - private void handleEllipseEnd() { - for (int i = mLines.size() - 1; i >= mMaxLines; i--) { - mLines.remove(mLines.get(i)); - } - Line lastLine = mLines.get(mLines.size() - 1); - int limitWidth = lastLine.getWidthLimit(); - Element ellipseElement = new CharOrPhraseElement("...", -1, -1); - ellipseElement.unsafeSingleEnvironmentUpdater(null, new EnvironmentUpdater() { - @Override - public void update(TypeEnvironment env) { - env.clear(); - } - }); - ellipseElement.measure(mTypeEnvironment); - limitWidth -= ellipseElement.getMeasureWidth(); - - Element moreElement = null; - if (mMoreText != null && !mMoreText.isEmpty()) { - moreElement = new CharOrPhraseElement(mMoreText, -1, -1); - List<Integer> changeTypes = new ArrayList<>(); - changeTypes.add(TypeEnvironment.TYPE_TEXT_COLOR); - changeTypes.add(TypeEnvironment.TYPE_BG_COLOR); - changeTypes.add(TypeEnvironment.TYPE_TYPEFACE); - changeTypes.add(TypeEnvironment.TYPE_BORDER_BOTTOM_COLOR); - changeTypes.add(TypeEnvironment.TYPE_BORDER_BOTTOM_WIDTH); - moreElement.unsafeSingleEnvironmentUpdater(changeTypes, new EnvironmentUpdater() { - @Override - public void update(TypeEnvironment env) { - if (mMoreTextColor != 0) { - env.setTextColor(mMoreTextColor); - } - if (mMoreBgColor != 0) { - env.setBackgroundColor(mMoreBgColor); - } - if (mMoreTextTypeface != null) { - env.setTypeface(mMoreTextTypeface); - } - - if (mMoreUnderlineHeight > 0) { - env.setBorderBottom(mMoreUnderlineHeight, mMoreUnderlineColor); - } - } - }); - moreElement.measure(mTypeEnvironment); - limitWidth -= moreElement.getMeasureWidth(); - } - - int contentWidth = lastLine.getContentWidth(); - if (contentWidth < limitWidth) { - lastLine.restoreVisibleChange(); - } else { - List<Element> elements = lastLine.popAll(); - for (Element el : elements) { - if (el.getMeasureWidth() <= limitWidth) { - lastLine.add(el); - limitWidth -= el.getMeasureWidth(); - } else { - break; - } - } - } - lastLine.add(ellipseElement); - if (moreElement != null) { - lastLine.add(moreElement); - } - lastLine.layout(mTypeEnvironment, mDropLastIfSpace, true); - } - - private void handleEllipseStart() { - mTypeEnvironment.clear(); - for (int i = mLines.size() - 1; i >= mMaxLines; i--) { - mLines.remove(mLines.get(i)); - } - Element ellipseElement = new CharOrPhraseElement("...", -1, -1); - ellipseElement.unsafeSingleEnvironmentUpdater(null, new EnvironmentUpdater() { - @Override - public void update(TypeEnvironment env) { - env.clear(); - } - }); - ellipseElement.measure(mTypeEnvironment); - Queue<Element> elements = new LinkedList<>(); - elements.add(ellipseElement); - for (int i = 0; i < mLines.size(); i++) { - Line line = mLines.get(i); - int limitWidth = line.getWidthLimit(); - elements.addAll(line.popAll()); - while (!elements.isEmpty()) { - Element el = elements.peek(); - if (el != null) { - if (el instanceof NextParagraphElement) { - elements.poll(); - line.add(el); - el.move(mTypeEnvironment); - break; - } - - if (el instanceof BreakWordLineElement) { - elements.poll(); - continue; - } - if (line.getContentWidth() + el.getMeasureWidth() <= limitWidth) { - elements.poll(); - line.add(el); - el.move(mTypeEnvironment); - - } else { - break; - } - } else { - elements.poll(); - } - } - line.handleWordBreak(mTypeEnvironment); - line.layout(mTypeEnvironment, mDropLastIfSpace, false); - if (elements.isEmpty()) { - return; - } - } - } - - private void handleEllipseMiddle() { - mTypeEnvironment.clear(); - List<Line> lines = new ArrayList<>(mLines); - mLines.clear(); - Element ellipseElement = new CharOrPhraseElement("...", -1, -1); - ellipseElement.measure(mTypeEnvironment); - int ellipseLine = mMaxLines % 2 == 0 ? mMaxLines / 2 : (mMaxLines + 1) / 2; - - for (int i = 0; i < ellipseLine; i++) { - mLines.add(lines.get(i)); - } - Line handleLine = lines.get(ellipseLine - 1); - int limitWidth = handleLine.getWidthLimit(); - Deque<Element> unHandled = new LinkedList<>(handleLine.popAll()); - while (!unHandled.isEmpty()) { - Element el = unHandled.peek(); - if (el != null) { - if (handleLine.getContentWidth() + el.getMeasureWidth() <= limitWidth / 2f - ellipseElement.getMeasureWidth() / 2) { - unHandled.poll(); - handleLine.add(el); - el.move(mTypeEnvironment); - } else { - break; - } - } else { - unHandled.poll(); - } - } - ellipseElement.measure(mTypeEnvironment); - handleLine.add(ellipseElement); - - int nextFullShowLine = lines.size() - mMaxLines + ellipseLine; - int startLine = lines.size() - 1; - // find the latest paragraph end line. - for (int i = lines.size() - 2; i > nextFullShowLine; i--) { - if (lines.get(i).isMiddleParagraphEndLine()) { - startLine = i; - } - } - for (int i = ellipseLine; i <= startLine; i++) { - unHandled.addAll(lines.get(i).popAll()); - } - - for (int i = startLine; i >= nextFullShowLine; i--) { - Line line = lines.get(i); - while (!unHandled.isEmpty()) { - Element element = unHandled.peekLast(); - if (element != null) { - if (element instanceof NextParagraphElement) { - unHandled.pollLast(); - continue; - } - if (element instanceof BreakWordLineElement) { - unHandled.pollLast(); - continue; - } - if (line.getContentWidth() + element.getMeasureWidth() <= line.getWidthLimit()) { - unHandled.pollLast(); - line.addFirst(element); - } else { - break; - } - } else { - unHandled.pollLast(); - } - } - } - - List<Element> toAdd = new LinkedList<>(); - int toAddWidth = 0; - while (!unHandled.isEmpty()) { - Element element = unHandled.peekLast(); - if (element != null) { - if (element instanceof NextParagraphElement) { - unHandled.pollLast(); - continue; - } - if (element instanceof BreakWordLineElement) { - unHandled.pollLast(); - continue; - } - if (handleLine.getContentWidth() + toAddWidth + element.getMeasureWidth() <= handleLine.getWidthLimit()) { - unHandled.pollLast(); - toAdd.add(0, element); - toAddWidth += element.getMeasureWidth(); - } else { - break; - } - } else { - unHandled.pollLast(); - } - } - - Element firstUnHandle = unHandled.peekFirst(); - Element lastUnHandle = unHandled.peekLast(); - Element effect = mTypeModel.getFirstEffect(); - if (firstUnHandle != null && lastUnHandle != null) { - List<Element> ellipseEffect = new ArrayList<>(); - while (effect != null && effect.getIndex() <= lastUnHandle.getIndex()) { - if (effect.getIndex() >= firstUnHandle.getIndex()) { - ellipseEffect.add(effect); - } - effect = effect.getNext(); - } - if (ellipseEffect.size() > 0) { - EffectElement effectElement = new EffectElement(ellipseEffect); - effectElement.move(mTypeEnvironment); - handleLine.add(effectElement); - } - } - for (Element el : toAdd) { - el.move(mTypeEnvironment); - handleLine.add(el); - } - handleLine.handleWordBreak(mTypeEnvironment); - handleLine.layout(mTypeEnvironment, mDropLastIfSpace, ellipseLine == lines.size()); - int lastEnd = handleLine.getY() + handleLine.getContentHeight(); - for (int i = nextFullShowLine; i < lines.size(); i++) { - Line line = lines.get(i); - Line prev = lines.get(i - 1); - if (prev.isMiddleParagraphEndLine()) { - line.setY(lastEnd + mTypeEnvironment.getParagraphSpace()); - } else { - line.setY(lastEnd + mTypeEnvironment.getLineSpace()); - } - lastEnd = line.getY() + line.getContentHeight(); - line.move(mTypeEnvironment); - line.handleWordBreak(mTypeEnvironment); - line.layout(mTypeEnvironment, mDropLastIfSpace, i == lines.size() - 1); - mLines.add(line); - } - } - - public int getMaxLayoutWidth() { - int maxWidth = 0; - for (Line line : mLines) { - maxWidth = Math.max(maxWidth, line.getLayoutWidth()); - } - return maxWidth; - } - - public int getContentHeight() { - if (mLines.isEmpty()) { - return 0; - } - Line last = mLines.get(mLines.size() - 1); - return last.getY() + last.getContentHeight(); - } - - public void draw(Canvas canvas) { - mTypeEnvironment.clear(); - for (Line line : mLines) { - line.draw(mTypeEnvironment, canvas); - } - } - - private boolean canInterrupt() { - return mLines.size() == mMaxLines && !mCalculateWholeLines && - (mEllipsize == null || mEllipsize == TextUtils.TruncateAt.END); - } - - public void release() { - for (Line line : mLines) { - line.release(); - } - mLines.clear(); - } -} diff --git a/type/src/main/java/com/qmuiteam/qmui/type/LineLayout.kt b/type/src/main/java/com/qmuiteam/qmui/type/LineLayout.kt new file mode 100644 index 000000000..617b0ed41 --- /dev/null +++ b/type/src/main/java/com/qmuiteam/qmui/type/LineLayout.kt @@ -0,0 +1,475 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.qmuiteam.qmui.type + +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Typeface +import android.text.TextUtils.TruncateAt +import com.qmuiteam.qmui.type.element.* +import java.util.* + +class LineLayout { + var maxLines = Int.MAX_VALUE + var ellipsize: TruncateAt? = null + var calculateWholeLines = false + var dropLastIfSpace = true + var moreText: String? = null + var moreTextColor = 0 + var moreTextTypeface: Typeface? = null + var moreUnderlineColor = Color.TRANSPARENT + var moreBgColor = 0 + var moreUnderlineHeight = 0 + var moreTextFixAtEnd: Boolean = true + var typeModel: TypeModel? = null + var shouldHandleWordBreak: Boolean = true + + var lineIndentHandler:LineIndentHandler? = null + + private var exactlyHeightMaxLine = Int.MAX_VALUE + + private val mLines: MutableList<Line> = ArrayList() + + var totalLineCount = 0 + private set + + val lineCount: Int + get() { + return mLines.size + } + + fun getLine(i: Int): Line? { + return mLines.getOrNull(i) + } + + fun measureAndLayout(env: TypeEnvironment, exactlyHeight: Boolean) { + exactlyHeightMaxLine = Int.MAX_VALUE + env.clear() + release() + lineIndentHandler?.reset() + if (typeModel == null) { + return + } + var element: Element? = typeModel!!.firstElement() + var line = Line.acquire() + var y = 0 + val indent = element?.let { lineIndentHandler?.processIndent(typeModel!!, it, true) } ?: 0 + line.init(indent, y, env.widthLimit - indent) + + fun addLineAndHandleMaxLineAndNextY(line: Line, isParagraphEndLine: Boolean){ + mLines.add(line) + if(env.lineHeight != -1){ + y += env.lineHeight.coerceAtLeast(line.contentHeight) + checkExactlyHeightMaxLine(env, y, exactlyHeight) + if(isParagraphEndLine){ + y += env.paragraphSpace + } + }else{ + y += line.contentHeight + checkExactlyHeightMaxLine(env, y, exactlyHeight) + y += if(isParagraphEndLine){ + env.paragraphSpace + }else{ + env.lineSpace + } + } + } + + while (element != null) { + element.measure(env) + if (element is NextParagraphElement) { + line.add(element) + line.layout(env, dropLastIfSpace, false) + addLineAndHandleMaxLineAndNextY(line, true) + if (canInterrupt(element)) { + handleEllipse(env,true) + totalLineCount = mLines.size + return + } + line = createNewLine(env, element.next, y, true) + } else if (line.contentWidth + element.measureWidth > line.widthLimit) { + if (mLines.size == 0 && line.size == 0) { + // the width is too small. + line.release() + totalLineCount = mLines.size + return + } + val back = line.handleWordBreak(env, shouldHandleWordBreak) + line.layout(env, dropLastIfSpace, false) + addLineAndHandleMaxLineAndNextY(line, false) + + if (canInterrupt(element)) { + totalLineCount = mLines.size + handleEllipse(env,true) + return + } + + line = createNewLine(env, back?.firstOrNull() ?: element, y, false) + if (back != null && back.isNotEmpty()) { + for (el in back) { + line.add(el) + } + } + line.add(element) + } else { + line.add(element) + } + element = element.next + } + if (line.size > 0) { + line.layout(env, dropLastIfSpace, true) + addLineAndHandleMaxLineAndNextY(line, false) + } else { + line.release() + } + totalLineCount = mLines.size + handleEllipse(env,false) + } + + private fun checkExactlyHeightMaxLine(env: TypeEnvironment, y: Int, exactlyHeight: Boolean){ + if(exactlyHeight && exactlyHeightMaxLine == Int.MAX_VALUE){ + if(y > env.heightLimit){ + exactlyHeightMaxLine = mLines.size - 1 + }else if(y == env.heightLimit){ + exactlyHeightMaxLine = mLines.size + } + } + } + + private fun createNewLine(env: TypeEnvironment, firstElement: Element?, y: Int, newParagraph: Boolean): Line { + val line = Line.acquire() + val indent = firstElement?.let { lineIndentHandler?.processIndent(typeModel!!, it, newParagraph) } ?: 0 + line.init(indent, y, env.widthLimit - indent) + return line + } + + private fun handleEllipse(env: TypeEnvironment, fromInterrupt: Boolean) { + if (mLines.isEmpty() || mLines.size < getUsedMaxLine() || (mLines.size == getUsedMaxLine() && !fromInterrupt)) { + return + } + if (ellipsize == TruncateAt.END) { + handleEllipseEnd(env) + } else if (ellipsize == TruncateAt.START) { + handleEllipseStart(env) + } else if (ellipsize == TruncateAt.MIDDLE) { + handleEllipseMiddle(env) + } + } + + private fun handleEllipseEnd(env: TypeEnvironment) { + val maxSize = getUsedMaxLine() + while (mLines.size > maxSize){ + val line = mLines[mLines.size - 1] + mLines.remove(line) + line.release() + } + + if(mLines.isEmpty()){ + return + } + + val lastLine = mLines[mLines.size - 1] + var limitWidth = lastLine.widthLimit + val ellipseElement: Element = TextElement("...", -1, -1) + ellipseElement.addSingleEnvironmentUpdater(null) { it.clear() } + ellipseElement.measure(env) + limitWidth -= ellipseElement.measureWidth + var moreElement: Element? = null + if (moreText != null && moreText!!.isNotEmpty()) { + moreElement = MoreTextElement(moreText!!, -1, -1) + val changeTypes: MutableList<Int> = ArrayList() + changeTypes.add(TypeEnvironment.TYPE_TEXT_COLOR) + changeTypes.add(TypeEnvironment.TYPE_BG_COLOR) + changeTypes.add(TypeEnvironment.TYPE_TYPEFACE) + changeTypes.add(TypeEnvironment.TYPE_BORDER_BOTTOM_COLOR) + changeTypes.add(TypeEnvironment.TYPE_BORDER_BOTTOM_WIDTH) + moreElement.addSingleEnvironmentUpdater(changeTypes, object : EnvironmentUpdater { + override fun update(env: TypeEnvironment) { + if (moreTextColor != 0) { + env.textColor = moreTextColor + } + if (moreBgColor != 0) { + env.backgroundColor = moreBgColor + } + if (moreTextTypeface != null) { + env.typeface = moreTextTypeface + } + if (moreUnderlineHeight > 0) { + env.setBorderBottom(moreUnderlineHeight, moreUnderlineColor) + } + } + }) + moreElement.measure(env) + limitWidth -= moreElement.measureWidth + } + val contentWidth = lastLine.contentWidth + if (contentWidth < limitWidth) { + lastLine.restoreVisibleChange() + } else { + val elements = lastLine.popAll() + for (el in elements) { + if (el.measureWidth <= limitWidth) { + lastLine.add(el) + limitWidth -= el.measureWidth + } else { + break + } + } + } + lastLine.add(ellipseElement) + if (moreElement != null) { + lastLine.add(moreElement) + } + lastLine.layout(env, dropLastIfSpace, true) + if (moreElement != null) { + if(moreTextFixAtEnd){ + moreElement.x = lastLine.x + lastLine.widthLimit - moreElement.measureWidth + }else{ + moreElement.x = lastLine.contentWidth - moreElement.measureWidth + } + + } + } + + private fun handleEllipseStart(env: TypeEnvironment) { + env.clear() + val tmpList = mutableListOf<Line>() + var tmpY = -1 + val startIndex = mLines.size - getUsedMaxLine() + val minus = mLines[startIndex].y + for(i in startIndex until mLines.size){ + mLines[i].y -= minus + tmpY += mLines[i].contentHeight + tmpList.add(mLines[i]) + } + mLines.clear() + mLines.addAll(tmpList) + val ellipseElement: Element = TextElement("...", -1, -1) + ellipseElement.addSingleEnvironmentUpdater(null, object : EnvironmentUpdater { + override fun update(env: TypeEnvironment) { + env.clear() + } + }) + ellipseElement.measure(env) + val elements: Queue<Element> = LinkedList() + elements.add(ellipseElement) + for (i in mLines.indices) { + val line = mLines[i] + val limitWidth = line.widthLimit + elements.addAll(line.popAll()) + while (!elements.isEmpty()) { + val el = elements.peek() + if (el != null) { + if (el is NextParagraphElement) { + elements.poll() + line.add(el) + el.move(env) + break + } + if (el is BreakWordLineElement) { + elements.poll() + continue + } + if (line.contentWidth + el.measureWidth <= limitWidth) { + elements.poll() + line.add(el) + el.move(env) + } else { + break + } + } else { + elements.poll() + } + } + line.handleWordBreak(env, shouldHandleWordBreak) + line.layout(env, dropLastIfSpace, false) + if (elements.isEmpty()) { + return + } + } + } + + private fun handleEllipseMiddle(env: TypeEnvironment) { + env.clear() + val lines: List<Line> = ArrayList(mLines) + mLines.clear() + val ellipseElement: Element = TextElement("...", -1, -1) + ellipseElement.measure(env) + val maxLines = getUsedMaxLine() + val ellipseLine = if (maxLines % 2 == 0) maxLines / 2 else (maxLines + 1) / 2 + for (i in 0 until ellipseLine) { + mLines.add(lines[i]) + } + val handleLine = lines[ellipseLine - 1] + val limitWidth = handleLine.widthLimit + val unHandled: Deque<Element> = LinkedList(handleLine.popAll()) + while (!unHandled.isEmpty()) { + val el = unHandled.peek() + if (el != null) { + if (handleLine.contentWidth + el.measureWidth <= limitWidth / 2f - ellipseElement.measureWidth / 2) { + unHandled.poll() + handleLine.add(el) + el.move(env) + } else { + break + } + } else { + unHandled.poll() + } + } + ellipseElement.measure(env) + handleLine.add(ellipseElement) + val nextFullShowLine = lines.size - maxLines + ellipseLine + var startLine = lines.size - 1 + // find the latest paragraph end line. + for (i in lines.size - 2 downTo nextFullShowLine + 1) { + if (lines[i].isMiddleParagraphEndLine) { + startLine = i + } + } + for (i in ellipseLine..startLine) { + unHandled.addAll(lines[i].popAll()) + } + for (i in startLine downTo nextFullShowLine) { + val line = lines[i] + while (!unHandled.isEmpty()) { + val element = unHandled.peekLast() + if (element != null) { + if (element is NextParagraphElement) { + unHandled.pollLast() + continue + } + if (element is BreakWordLineElement) { + unHandled.pollLast() + continue + } + if (line.contentWidth + element.measureWidth <= line.widthLimit) { + unHandled.pollLast() + line.addFirst(element) + } else { + break + } + } else { + unHandled.pollLast() + } + } + } + val toAdd = LinkedList<Element>() + var toAddWidth = 0 + while (!unHandled.isEmpty()) { + val element = unHandled.peekLast() + if (element != null) { + if (element is NextParagraphElement) { + unHandled.pollLast() + continue + } + if (element is BreakWordLineElement) { + unHandled.pollLast() + continue + } + if (handleLine.contentWidth + toAddWidth + element.measureWidth <= handleLine.widthLimit) { + unHandled.pollLast() + toAdd.add(0, element) + toAddWidth = (toAddWidth + element.measureWidth).toInt() + } else { + break + } + } else { + unHandled.pollLast() + } + } + val firstUnHandle = unHandled.peekFirst() + val lastUnHandle = unHandled.peekLast() + var effect = typeModel!!.firstEffect + if (firstUnHandle != null && lastUnHandle != null) { + val ellipseEffect: MutableList<Element> = ArrayList() + while (effect != null && effect.index <= lastUnHandle.index) { + if (effect.index >= firstUnHandle.index) { + ellipseEffect.add(effect) + } + effect = effect.next + } + if (ellipseEffect.size > 0) { + val ignoreEffectElement = IgnoreEffectElement(ellipseEffect) + ignoreEffectElement.move(env) + handleLine.add(ignoreEffectElement) + } + } + for (el in toAdd) { + el.move(env) + handleLine.add(el) + } + handleLine.handleWordBreak(env, shouldHandleWordBreak) + handleLine.layout(env, dropLastIfSpace, ellipseLine == lines.size) + var lastEnd = handleLine.y + handleLine.contentHeight + for (i in nextFullShowLine until lines.size) { + val line = lines[i] + val prev = lines[i - 1] + if (prev.isMiddleParagraphEndLine) { + line.y = lastEnd + env.paragraphSpace.coerceAtLeast(env.lineHeight - handleLine.contentHeight) + } else { + line.y = lastEnd + env.lineSpace.coerceAtLeast(env.lineHeight - handleLine.contentHeight) + } + lastEnd = line.y + line.contentHeight + line.move(env) + line.handleWordBreak(env, shouldHandleWordBreak) + line.layout(env, dropLastIfSpace, i == lines.size - 1) + mLines.add(line) + } + } + + val maxLayoutWidth: Int + get() { + var maxWidth = 0 + for (line in mLines) { + maxWidth = line.layoutWidth.coerceAtLeast(maxWidth) + } + return maxWidth + } + val contentHeight: Int + get() { + if (mLines.isEmpty()) { + return 0 + } + val last = mLines[mLines.size - 1] + return last.y + last.contentHeight + } + + fun draw(canvas: Canvas, env: TypeEnvironment) { + env.clear() + for (line in mLines) { + line.draw(env, canvas) + } + } + + private fun canInterrupt(element: Element): Boolean { + return mLines.size == getUsedMaxLine() && + !calculateWholeLines && + (ellipsize == null || ellipsize == TruncateAt.END) && + element.next != null + } + + private fun getUsedMaxLine(): Int { + return maxLines.coerceAtMost(exactlyHeightMaxLine) + } + + fun release() { + for (line in mLines) { + line.release() + } + mLines.clear() + } +} \ No newline at end of file diff --git a/type/src/main/java/com/qmuiteam/qmui/type/TypeEnvironment.java b/type/src/main/java/com/qmuiteam/qmui/type/TypeEnvironment.java deleted file mode 100644 index 29462399b..000000000 --- a/type/src/main/java/com/qmuiteam/qmui/type/TypeEnvironment.java +++ /dev/null @@ -1,347 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, - * either express or implied. See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.qmuiteam.qmui.type; - -import android.content.res.Resources; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Typeface; -import android.util.Log; -import android.util.SparseArray; - -import com.qmuiteam.qmui.type.element.Element; - -import java.util.Stack; - -public class TypeEnvironment { - private static final String TAG = "TypeEnvironment"; - public static final int TYPE_TEXT_COLOR = -1; - public static final int TYPE_BG_COLOR = -2; - public static final int TYPE_TYPEFACE = -3; - public static final int TYPE_TEXT_SIZE = -4; - public static final int TYPE_ALIGNMENT = -5; - public static final int TYPE_LINE_SPACE = -6; - public static final int TYPE_PARAGRAPH_SPACE = -7; - public static final int TYPE_BORDER_TOP_WIDTH = -8; - public static final int TYPE_BORDER_TOP_COLOR = -9; - public static final int TYPE_BORDER_RIGHT_WIDTH = -10; - public static final int TYPE_BORDER_RIGHT_COLOR = -11; - public static final int TYPE_BORDER_BOTTOM_WIDTH = -12; - public static final int TYPE_BORDER_BOTTOM_COLOR = -13; - public static final int TYPE_BORDER_LEFT_WIDTH = -14; - public static final int TYPE_BORDER_LEFT_COLOR = -15; - public static final int TYPE_BORDER_PAINT = -16; - - public enum Alignment { - LEFT, - RIGHT, - CENTER, - JUSTIFY - } - - private int mWidthLimit; - private int mHeightLimit; - - private Typeface mTypeface; - private float mTextSize; - private int mLineSpace; - private int mParagraphSpace; - private Alignment mAlignment = Alignment.JUSTIFY; - private int mLastLineJustifyMaxWidth = (int) (Resources.getSystem().getDisplayMetrics().density * 36); - - private int mTextColor = Color.BLACK; - private int mBackgroundColor; - - private Paint mPaint = new Paint(); - private Paint mBgPaint = new Paint(); - - private SparseArray<Object> mCustomProp = new SparseArray<>(); - - private SparseArray<Stack<Object>> mStack = new SparseArray<>(); - private Element mLastRunElement = null; - - - public TypeEnvironment() { - mPaint.setAntiAlias(true); - mBgPaint.setAntiAlias(true); - } - - - public void setMeasureLimit(int widthLimit, int heightLimit) { - mWidthLimit = widthLimit; - mHeightLimit = heightLimit; - } - - public int getWidthLimit() { - return mWidthLimit; - } - - public int getHeightLimit() { - return mHeightLimit; - } - - public void setLastLineJustifyMaxWidth(int lastLineJustifyMaxWidth) { - mLastLineJustifyMaxWidth = lastLineJustifyMaxWidth; - } - - public int getLastLineJustifyMaxWidth() { - return mLastLineJustifyMaxWidth; - } - - public void setTypeface(Typeface typeface) { - mTypeface = typeface; - mPaint.setTypeface(typeface); - } - - public void setTextColor(int textColor) { - mTextColor = textColor; - mPaint.setColor(textColor); - } - - public void setTextSize(float textSize) { - mTextSize = textSize; - mPaint.setTextSize(textSize); - } - - public void setBackgroundColor(int backgroundColor) { - mBackgroundColor = backgroundColor; - mBgPaint.setColor(backgroundColor); - } - - public void setAlignment(Alignment alignment) { - mAlignment = alignment; - } - - public void setLineSpace(int lineSpace) { - mLineSpace = lineSpace; - } - - public void setParagraphSpace(int paragraphSpace) { - mParagraphSpace = paragraphSpace; - } - - public int getParagraphSpace() { - return Math.max(mParagraphSpace, mLineSpace); - } - - public Alignment getAlignment() { - return mAlignment; - } - - public int getLineSpace() { - return mLineSpace; - } - - public float getTextSize() { - return mTextSize; - } - - public int getTextColor() { - return mTextColor; - } - - public int getBackgroundColor() { - return mBackgroundColor; - } - - public Paint getBgPaint() { - return mBgPaint; - } - - public Paint getPaint() { - return mPaint; - } - - public void setCustomProp(int type, Object value) { - mCustomProp.put(type, value); - } - - public void setBorderTop(int width, int color) { - setCustomProp(TYPE_BORDER_TOP_WIDTH, width); - setCustomProp(TYPE_BORDER_TOP_COLOR, color); - } - - public int getBorderTopWidth() { - return getIntCustomProp(TYPE_BORDER_TOP_WIDTH); - } - - public int getBorderTopColor() { - return getIntCustomProp(TYPE_BORDER_TOP_COLOR); - } - - public void setBorderRight(int width, int color) { - setCustomProp(TYPE_BORDER_RIGHT_WIDTH, width); - setCustomProp(TYPE_BORDER_RIGHT_COLOR, color); - } - - public int getBorderRightWidth() { - return getIntCustomProp(TYPE_BORDER_RIGHT_WIDTH); - } - - public int getBorderRightColor() { - return getIntCustomProp(TYPE_BORDER_RIGHT_COLOR); - } - - public void setBorderBottom(int width, int color) { - setCustomProp(TYPE_BORDER_BOTTOM_WIDTH, width); - setCustomProp(TYPE_BORDER_BOTTOM_COLOR, color); - } - - public int getBorderBottomWidth() { - return getIntCustomProp(TYPE_BORDER_BOTTOM_WIDTH); - } - - public int getBorderBottomColor() { - return getIntCustomProp(TYPE_BORDER_BOTTOM_COLOR); - } - - public void setBorderLeft(int width, int color) { - setCustomProp(TYPE_BORDER_LEFT_WIDTH, width); - setCustomProp(TYPE_BORDER_LEFT_COLOR, color); - } - - public int getBorderLeftWidth() { - return getIntCustomProp(TYPE_BORDER_LEFT_WIDTH); - } - - public int getBorderLeftColor() { - return getIntCustomProp(TYPE_BORDER_LEFT_COLOR); - } - - public Paint getBorderPaint() { - Object obj = getCustomProp(TYPE_BORDER_PAINT); - Paint paint; - if (obj == null) { - paint = new Paint(); - paint.setAntiAlias(true); - setCustomProp(TYPE_BORDER_PAINT, paint); - } else { - paint = (Paint) obj; - } - return paint; - } - - public Object getCustomProp(int type) { - return mCustomProp.get(type); - } - - public int getIntCustomProp(int type) { - Object obj = mCustomProp.get(type); - if (!(obj instanceof Integer)) { - return 0; - } - return (int) obj; - } - - public TypeEnvironment snapshot() { - TypeEnvironment env = new TypeEnvironment(); - env.setMeasureLimit(mWidthLimit, mHeightLimit); - env.setAlignment(mAlignment); - env.setLineSpace(mLineSpace); - env.setParagraphSpace(mParagraphSpace); - env.setTextSize(mTextSize); - env.setTypeface(mTypeface); - - env.setTextColor(mTextColor); - env.setBackgroundColor(mBackgroundColor); - for(int i =0; i< mStack.size(); i++){ - env.mStack.put(mStack.keyAt(i), (Stack<Object>) mStack.valueAt(i).clone()); - } - - if (mCustomProp != null) { - for (int i = 0; i < mCustomProp.size(); i++) { - env.setCustomProp(mCustomProp.keyAt(i), mCustomProp.valueAt(i)); - } - } - return env; - } - - void setLastRunElement(Element lastRunElement) { - mLastRunElement = lastRunElement; - } - - Element getLastRunElement() { - return mLastRunElement; - } - - public void save(int type) { - Stack<Object> stack = mStack.get(type); - if (stack == null) { - stack = new Stack<>(); - mStack.put(type, stack); - } - if (type == TYPE_TEXT_COLOR) { - stack.push(mTextColor); - } else if (type == TYPE_BG_COLOR) { - stack.push(mBackgroundColor); - } else if (type == TYPE_TYPEFACE) { - stack.push(mTypeface); - } else if (type == TYPE_TEXT_SIZE) { - stack.push(mTextSize); - } else if (type == TYPE_ALIGNMENT) { - stack.push(mAlignment); - } else if (type == TYPE_LINE_SPACE) { - stack.push(mLineSpace); - } else if (type == TYPE_PARAGRAPH_SPACE) { - stack.push(mParagraphSpace); - } else { - stack.push(mCustomProp.get(type)); - } - } - - public void restore(int type) { - Stack<Object> stack = mStack.get(type); - if (stack == null || stack.isEmpty()) { - Log.d(TAG, "restore (type = " + type + ")with a empty stack."); - return; - } - Object v = stack.pop(); - restore(type, v); - } - - private void restore(int type, Object v){ - if (type == TYPE_TEXT_COLOR) { - setTextColor((Integer) v); - } else if (type == TYPE_BG_COLOR) { - setBackgroundColor((Integer) v); - } else if (type == TYPE_TYPEFACE) { - setTypeface((Typeface) v); - } else if (type == TYPE_TEXT_SIZE) { - setTextSize((Float) v); - } else if (type == TYPE_ALIGNMENT) { - setAlignment((Alignment) v); - } else if (type == TYPE_LINE_SPACE) { - setLineSpace((Integer) v); - } else if (type == TYPE_PARAGRAPH_SPACE) { - setParagraphSpace((Integer) v); - } else { - setCustomProp(type, v); - } - } - - public void clear() { - for (int i = 0; i < mStack.size(); i++) { - Stack<Object> stack = mStack.valueAt(i); - if (stack != null && stack.size() > 0) { - while (stack.size() > 1){ - stack.pop(); - } - restore(mStack.keyAt(i), stack.pop()); - } - } - } -} diff --git a/type/src/main/java/com/qmuiteam/qmui/type/TypeEnvironment.kt b/type/src/main/java/com/qmuiteam/qmui/type/TypeEnvironment.kt new file mode 100644 index 000000000..ca1a9b84a --- /dev/null +++ b/type/src/main/java/com/qmuiteam/qmui/type/TypeEnvironment.kt @@ -0,0 +1,279 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.qmuiteam.qmui.type + +import android.content.res.Resources +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Rect +import android.graphics.Typeface +import android.util.Log +import android.util.SparseArray +import java.util.* + +class TypeEnvironment { + companion object { + private const val TAG = "TypeEnvironment" + const val TYPE_TEXT_COLOR = -1 + const val TYPE_BG_COLOR = -2 + const val TYPE_TYPEFACE = -3 + const val TYPE_TEXT_SIZE = -4 + const val TYPE_ALIGNMENT = -5 + const val TYPE_LINE_SPACE = -6 + const val TYPE_PARAGRAPH_SPACE = -7 + const val TYPE_BORDER_TOP_WIDTH = -8 + const val TYPE_BORDER_TOP_COLOR = -9 + const val TYPE_BORDER_RIGHT_WIDTH = -10 + const val TYPE_BORDER_RIGHT_COLOR = -11 + const val TYPE_BORDER_BOTTOM_WIDTH = -12 + const val TYPE_BORDER_BOTTOM_COLOR = -13 + const val TYPE_BORDER_LEFT_WIDTH = -14 + const val TYPE_BORDER_LEFT_COLOR = -15 + const val TYPE_BORDER_PAINT = -16 + const val TYPE_LINE_HEIGHT = -17 + + val DEFAULT_LAST_LINE_JUSTIFY_MAX_WIDTH = (Resources.getSystem().displayMetrics.density * 36).toInt() + } + + enum class Alignment { + LEFT, RIGHT, CENTER, JUSTIFY + } + + var widthLimit = 0 + private set + var heightLimit = 0 + private set + + + var alignment: Alignment = Alignment.JUSTIFY + + var lastLineJustifyMaxWidth = DEFAULT_LAST_LINE_JUSTIFY_MAX_WIDTH + + val paint = Paint().apply { + isAntiAlias = true + textSize = Resources.getSystem().displayMetrics.scaledDensity * 14f + } + val bgPaint = Paint().apply { + isAntiAlias = true + } + private val mCustomProp: SparseArray<Any?> = SparseArray() + private val mStack = SparseArray<Stack<Any?>>() + + var workBreakMaxTryLength: Int = 10 + + var lineSpace = 0 + + var lineHeight = -1 + + var paragraphSpace: Int = 0 + get() = field.coerceAtLeast(lineSpace) + + var typeface: Typeface? = null + set(value) { + field = value + paint.typeface = value + } + + var textSize: Float = paint.textSize + set(value) { + field = value + paint.textSize = value + } + var textColor: Int = Color.BLACK + set(value) { + field = value + paint.color = value + } + var backgroundColor: Int = Color.TRANSPARENT + set(value) { + field = value + bgPaint.color = value + } + + fun setCustomProp(type: Int, value: Any?) { + mCustomProp.put(type, value) + } + + fun setBorderTop(width: Int, color: Int) { + setCustomProp(TYPE_BORDER_TOP_WIDTH, width) + setCustomProp(TYPE_BORDER_TOP_COLOR, color) + } + + val borderTopWidth: Int + get() = getIntCustomProp(TYPE_BORDER_TOP_WIDTH) + val borderTopColor: Int + get() = getIntCustomProp(TYPE_BORDER_TOP_COLOR) + + fun setBorderRight(width: Int, color: Int) { + setCustomProp(TYPE_BORDER_RIGHT_WIDTH, width) + setCustomProp(TYPE_BORDER_RIGHT_COLOR, color) + } + + val borderRightWidth: Int + get() = getIntCustomProp(TYPE_BORDER_RIGHT_WIDTH) + val borderRightColor: Int + get() = getIntCustomProp(TYPE_BORDER_RIGHT_COLOR) + + fun setBorderBottom(width: Int, color: Int) { + setCustomProp(TYPE_BORDER_BOTTOM_WIDTH, width) + setCustomProp(TYPE_BORDER_BOTTOM_COLOR, color) + } + + val borderBottomWidth: Int + get() = getIntCustomProp(TYPE_BORDER_BOTTOM_WIDTH) + val borderBottomColor: Int + get() = getIntCustomProp(TYPE_BORDER_BOTTOM_COLOR) + + fun setBorderLeft(width: Int, color: Int) { + setCustomProp(TYPE_BORDER_LEFT_WIDTH, width) + setCustomProp(TYPE_BORDER_LEFT_COLOR, color) + } + + val borderLeftWidth: Int + get() = getIntCustomProp(TYPE_BORDER_LEFT_WIDTH) + val borderLeftColor: Int + get() = getIntCustomProp(TYPE_BORDER_LEFT_COLOR) + val borderPaint: Paint + get() { + val obj = getCustomProp(TYPE_BORDER_PAINT) + val paint: Paint + if (obj == null) { + paint = Paint() + paint.isAntiAlias = true + setCustomProp(TYPE_BORDER_PAINT, paint) + } else { + paint = obj as Paint + } + return paint + } + + fun getCustomProp(type: Int): Any? { + return mCustomProp[type] + } + + fun getIntCustomProp(type: Int): Int { + val obj = mCustomProp[type] + return if (obj !is Int) { + 0 + } else obj + } + + fun setMeasureLimit(widthLimit: Int, heightLimit: Int) { + this.widthLimit = widthLimit + this.heightLimit = heightLimit + } + + fun snapshot(): TypeEnvironment { + val env = TypeEnvironment() + env.setMeasureLimit(widthLimit, heightLimit) + env.alignment = alignment + env.lineSpace = lineSpace + env.lineHeight = lineHeight + env.paragraphSpace = paragraphSpace + env.textSize = textSize + env.typeface = typeface + env.textColor = textColor + env.backgroundColor = backgroundColor + for (i in 0 until mStack.size()) { + env.mStack.put(mStack.keyAt(i), mStack.valueAt(i).clone() as Stack<Any?>) + } + for (i in 0 until mCustomProp.size()) { + env.setCustomProp(mCustomProp.keyAt(i), mCustomProp.valueAt(i)) + } + return env + } + + fun save(type: Int) { + var stack = mStack[type] + if (stack == null) { + stack = Stack() + mStack.put(type, stack) + } + if (type == TYPE_TEXT_COLOR) { + stack.push(textColor) + } else if (type == TYPE_BG_COLOR) { + stack.push(backgroundColor) + } else if (type == TYPE_TYPEFACE) { + stack.push(typeface) + } else if (type == TYPE_TEXT_SIZE) { + stack.push(textSize) + } else if (type == TYPE_ALIGNMENT) { + stack.push(alignment) + } else if (type == TYPE_LINE_SPACE) { + stack.push(lineSpace) + } else if (type == TYPE_PARAGRAPH_SPACE) { + stack.push(paragraphSpace) + } else if(type == TYPE_LINE_HEIGHT){ + stack.push(lineHeight) + } else{ + stack.push(mCustomProp[type]) + } + } + + fun restore(type: Int) { + val stack = mStack[type] + if (stack == null || stack.isEmpty()) { + Log.d(TAG, "restore (type = $type)with a empty stack.") + return + } + val v = stack.pop() + restore(type, v) + } + + private fun restore(type: Int, v: Any?) { + if (type == TYPE_TEXT_COLOR) { + textColor = v as Int + } else if (type == TYPE_BG_COLOR) { + backgroundColor = v as Int + } else if (type == TYPE_TYPEFACE) { + typeface = v as? Typeface + } else if (type == TYPE_TEXT_SIZE) { + textSize = v as Float + } else if (type == TYPE_ALIGNMENT) { + alignment = v as Alignment + } else if (type == TYPE_LINE_SPACE) { + lineSpace = v as Int + } else if (type == TYPE_PARAGRAPH_SPACE) { + paragraphSpace = v as Int + }else if (type == TYPE_LINE_HEIGHT) { + lineHeight = v as Int + } else { + setCustomProp(type, v) + } + } + + fun clear() { + for (i in 0 until mStack.size()) { + val stack = mStack.valueAt(i) + if (stack != null && stack.size > 0) { + while (stack.size > 1) { + stack.pop() + } + restore(mStack.keyAt(i), stack.pop()) + } + } + } + + fun isRunning(): Boolean{ + for (i in 0 until mStack.size()) { + val stack = mStack.valueAt(i) + if(stack.size > 0){ + return true + } + } + return false + } +} \ No newline at end of file diff --git a/type/src/main/java/com/qmuiteam/qmui/type/TypeModel.java b/type/src/main/java/com/qmuiteam/qmui/type/TypeModel.java deleted file mode 100644 index c93e10b71..000000000 --- a/type/src/main/java/com/qmuiteam/qmui/type/TypeModel.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, - * either express or implied. See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.qmuiteam.qmui.type; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.qmuiteam.qmui.type.element.Element; - -import java.util.Map; - -public class TypeModel { - private CharSequence mOrigin; - private final Map<Integer, Element> mElementMap; - private Element mFirstElement; - private Element mLastElement; - @Nullable - private Element mFirstEffect; - - public TypeModel(@NonNull Map<Integer, Element> elementMap, - Element firstElement, - Element lastElement, - @Nullable Element firstEffect) { - mElementMap = elementMap; - mFirstElement = firstElement; - mLastElement = lastElement; - mFirstEffect = firstEffect; - } - - @Nullable - public Element getFirstEffect() { - return mFirstEffect; - } - - public void insertAfterElement(Element element, @NonNull Element toInsert) { - Element next = element.getNext(); - element.setNext(toInsert); - toInsert.setNext(next); - if (next == null) { - mLastElement = toInsert; - } - } - - public void insertBeforeElement(Element element, @NonNull Element toInsert) { - Element prev = element.getPrev(); - element.setPrev(toInsert); - toInsert.setPrev(prev); - if (prev == null) { - mFirstElement = toInsert; - } - } - - public Element firstElement() { - return mFirstElement; - } - - public Element lastElement() { - return mLastElement; - } -} \ No newline at end of file diff --git a/type/src/main/java/com/qmuiteam/qmui/type/TypeModel.kt b/type/src/main/java/com/qmuiteam/qmui/type/TypeModel.kt new file mode 100644 index 000000000..f1a7feb74 --- /dev/null +++ b/type/src/main/java/com/qmuiteam/qmui/type/TypeModel.kt @@ -0,0 +1,156 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.qmuiteam.qmui.type + +import android.graphics.Typeface +import com.qmuiteam.qmui.type.element.Element + +class TypeModel( + val origin: CharSequence, + private val mElementMap: Map<Int, Element>, + private val mFirstElement: Element, + private val mLastElement: Element +) { + + var firstEffect: Element? = null + + fun addTypefaceEffect(start: Int, end: Int, typeface: Typeface): EffectRemover? { + val types: MutableList<Int> = ArrayList() + types.add(TypeEnvironment.TYPE_TYPEFACE) + return unsafeAddEffect(start, end, types) { env -> env.typeface = typeface } + } + + fun addTextSizeEffect(start: Int, end: Int, textSize: Float): EffectRemover? { + val types: MutableList<Int> = ArrayList() + types.add(TypeEnvironment.TYPE_TEXT_SIZE) + return unsafeAddEffect(start, end, types) { env -> env.textSize = textSize } + } + + fun addBgEffect(start: Int, end: Int, bgColor: Int): EffectRemover? { + val types: MutableList<Int> = ArrayList() + types.add(TypeEnvironment.TYPE_BG_COLOR) + return unsafeAddEffect(start, end, types) { env -> env.backgroundColor = bgColor } + } + + fun addTextColorEffect(start: Int, end: Int, textColor: Int): EffectRemover? { + val types: MutableList<Int> = ArrayList() + types.add(TypeEnvironment.TYPE_TEXT_COLOR) + return unsafeAddEffect(start, end, types) { env -> env.textColor = textColor } + } + + fun addUnderLineEffect(start: Int, end: Int, underLineColor: Int, underLineHeight: Int): EffectRemover? { + val types: MutableList<Int> = ArrayList() + types.add(TypeEnvironment.TYPE_BORDER_BOTTOM_WIDTH) + types.add(TypeEnvironment.TYPE_BORDER_BOTTOM_COLOR) + return unsafeAddEffect( + start, end, types + ) { env -> env.setBorderBottom(underLineHeight, underLineColor) } + } + + fun unsafeAddEffect(start: Int, end: Int, types: List<Int>, environmentUpdater: EnvironmentUpdater): EffectRemover? { + if (start > end) { + throw RuntimeException("unsafeAddEffect: start($start) is bigger than end($end)") + } + val elementStart = getByPos(start) + val elementEnd = getByPos(end) + if (elementStart == null || elementEnd == null) { + return null + } + for (type in types) { + elementStart.addSaveType(type) + elementEnd.addRestoreType(type) + } + elementStart.addEnvironmentUpdater(environmentUpdater) + firstEffect = if (firstEffect == null) { + elementStart + } else { + elementStart.insertEffectTo(firstEffect!!) + } + firstEffect = elementEnd.insertEffectTo(firstEffect!!) + return DefaultEffectRemove(this, start, end, types, environmentUpdater) + } + + fun unsafeRemoveEffect(start: Int, end: Int, types: List<Int>, environmentUpdater: EnvironmentUpdater): Boolean { + val elementStart = getByPos(start) + val elementEnd = getByPos(end) + if (elementStart == null || elementEnd == null) { + return false + } + for (type in types) { + elementStart.removeSaveType(type) + elementEnd.removeStoreType(type) + } + elementStart.removeEnvironmentUpdater(environmentUpdater) + firstEffect = elementStart.removeFromEffectListIfNeeded(firstEffect) + firstEffect = elementEnd.removeFromEffectListIfNeeded(firstEffect) + return true + } + + fun firstElement(): Element { + return mFirstElement + } + + fun lastElement(): Element { + return mLastElement + } + + + fun getByPos(pos: Int): Element? { + val anchor = mElementMap[pos] ?: mLastElement + val anchorEnd = anchor.start + anchor.text.length + if (anchor.start <= pos && anchorEnd > pos) { + return anchor + } else if (anchorEnd <= pos) { + var next = anchor.next + while (next != null) { + if (next.start + next.text.length > pos) { + return next + } + next = next.next + } + return null + } else { + var prev = anchor.prev + while (prev != null) { + if (prev.start <= pos) { + return prev + } + prev = prev.prev + } + return null + } + } + + fun getByIndex(pos: Int): Element? { + return mElementMap[pos] + } + + fun interface EffectRemover { + fun remove() + } +} + +class DefaultEffectRemove( + private val typeModel: TypeModel, + private val start: Int, + private val end: Int, + private val types: List<Int>, + private val environmentUpdater: EnvironmentUpdater +) : TypeModel.EffectRemover { + override fun remove() { + typeModel.unsafeRemoveEffect(start, end, types, environmentUpdater) + } +} \ No newline at end of file diff --git a/qmui/src/main/java/com/qmuiteam/qmui/widget/INotchInsetConsumer.java b/type/src/main/java/com/qmuiteam/qmui/type/element/BreakWordLineElement.kt similarity index 79% rename from qmui/src/main/java/com/qmuiteam/qmui/widget/INotchInsetConsumer.java rename to type/src/main/java/com/qmuiteam/qmui/type/element/BreakWordLineElement.kt index 7a9f39107..f728441cf 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/widget/INotchInsetConsumer.java +++ b/type/src/main/java/com/qmuiteam/qmui/type/element/BreakWordLineElement.kt @@ -13,13 +13,10 @@ * either express or implied. See the License for the specific language governing permissions and * limitations under the License. */ +package com.qmuiteam.qmui.type.element -package com.qmuiteam.qmui.widget; - -public interface INotchInsetConsumer { - /** - * - * @return if true stop dispatch to child view - */ - boolean notifyInsetMaybeChanged(); +class BreakWordLineElement : TextElement("-", -1, -1) { + init { + wordPart = WORD_PART_MIDDLE + } } \ No newline at end of file diff --git a/type/src/main/java/com/qmuiteam/qmui/type/element/CharOrPhraseElement.java b/type/src/main/java/com/qmuiteam/qmui/type/element/CharOrPhraseElement.java deleted file mode 100644 index 382dd48f9..000000000 --- a/type/src/main/java/com/qmuiteam/qmui/type/element/CharOrPhraseElement.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, - * either express or implied. See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.qmuiteam.qmui.type.element; - -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.qmuiteam.qmui.type.TypeEnvironment; - -public class CharOrPhraseElement extends Element { - - public CharOrPhraseElement(char singleChar, int index, int originIndex) { - super(singleChar, null, index, originIndex); - } - - public CharOrPhraseElement(char singleChar, int index, int originIndex, @Nullable String description) { - super(singleChar, null, index, originIndex, description); - } - - public CharOrPhraseElement(String text, int index, int originIndex) { - super('\u0000', text, index, originIndex); - } - - @Override - protected void onMeasure(TypeEnvironment env) { - Paint paint = env.getPaint(); - setMeasureDimen(paint.measureText(toString()), - paint.getFontMetrics().descent - paint.getFontMetrics().ascent, - -paint.getFontMetrics().ascent); - } - - @Override - protected void onDraw(TypeEnvironment env, Canvas canvas) { - canvas.drawText(toString(), getX(), getY() + getBaseLine(), env.getPaint()); - } -} diff --git a/type/src/main/java/com/qmuiteam/qmui/type/element/DrawableElement.java b/type/src/main/java/com/qmuiteam/qmui/type/element/DrawableElement.java deleted file mode 100644 index 167c75243..000000000 --- a/type/src/main/java/com/qmuiteam/qmui/type/element/DrawableElement.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.qmuiteam.qmui.type.element; - -import android.graphics.Canvas; -import android.graphics.drawable.Drawable; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.qmuiteam.qmui.type.TypeEnvironment; - -public class DrawableElement extends Element { - - private final Drawable mDrawable; - - public DrawableElement( - @NonNull Drawable drawable, - Character singleChar, - @Nullable CharSequence text, - int index, int originIndex) { - super(singleChar, text, index, originIndex, - text != null && text.length() > 2 && text.charAt(0) == '[' - ? text.subSequence(1, text.length() - 1).toString() : null); - mDrawable = drawable.mutate(); - mDrawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); - } - - @Override - protected void onMeasure(TypeEnvironment env) { - setMeasureDimen( - mDrawable.getIntrinsicWidth(), - mDrawable.getIntrinsicHeight(), - 0); - } - - @Override - protected void onDraw(TypeEnvironment env, Canvas canvas) { - canvas.save(); - canvas.translate(getX(), getY()); - mDrawable.draw(canvas); - canvas.restore(); - } -} diff --git a/type/src/main/java/com/qmuiteam/qmui/type/element/DrawableElement.kt b/type/src/main/java/com/qmuiteam/qmui/type/element/DrawableElement.kt new file mode 100644 index 000000000..ee1fa8d09 --- /dev/null +++ b/type/src/main/java/com/qmuiteam/qmui/type/element/DrawableElement.kt @@ -0,0 +1,43 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.qmuiteam.qmui.type.element + +import android.graphics.Canvas +import android.graphics.drawable.Drawable +import com.qmuiteam.qmui.type.TypeEnvironment + +class DrawableElement( + drawable: Drawable, + text: CharSequence, + index: Int, start: Int) : Element(text, index, start) { + + val drawable = drawable.mutate().apply { + setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight) + } + + override fun onMeasure(env: TypeEnvironment) { + setMeasureDimen(drawable.intrinsicWidth, drawable.intrinsicHeight, 0) + } + + override fun onDraw(env: TypeEnvironment, canvas: Canvas) { + drawBg(env, canvas) + canvas.save() + canvas.translate(x.toFloat(), y.toFloat()) + drawable.draw(canvas) + canvas.restore() + drawBorder(env, canvas) + } +} \ No newline at end of file diff --git a/type/src/main/java/com/qmuiteam/qmui/type/element/Element.java b/type/src/main/java/com/qmuiteam/qmui/type/element/Element.java deleted file mode 100644 index a9b813638..000000000 --- a/type/src/main/java/com/qmuiteam/qmui/type/element/Element.java +++ /dev/null @@ -1,304 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, - * either express or implied. See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.qmuiteam.qmui.type.element; - -import android.graphics.Canvas; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.qmuiteam.qmui.type.EnvironmentUpdater; -import com.qmuiteam.qmui.type.TypeEnvironment; - -import java.util.ArrayList; -import java.util.List; - -public abstract class Element { - public static final int VISIBLE = 0; - public static final int INVISIBLE = 1; - public static final int GONE = 2; - public static final int WORD_PART_WHOLE = 0; - public static final int WORD_PART_START = 1; - public static final int WORD_PART_MIDDLE = 2; - public static final int WORD_PART_END = 3; - private final char mChar; - private final CharSequence mText; - private final int mIndex; - private final int mOriginIndex; - private final String mDescription; - private Element mNextEffect; - private Element mPrev; - private Element mNext; - private int mWordPart = WORD_PART_WHOLE; - private boolean mCanBreakWord = false; - private int mVisible = VISIBLE; - - - private List<Integer> mSaveType; - private List<Integer> mRestoreType; - @Nullable - private List<EnvironmentUpdater> mEnvironmentUpdater; - - private float mMeasureWidth; - private float mMeasureHeight; - private float mX; - private float mY; - private float mBaseLine; - - public Element(Character singleChar, @Nullable CharSequence text, int index, int originIndex) { - this(singleChar, text, index, originIndex, null); - } - - public Element(char singleChar, @Nullable CharSequence text, int index, int originIndex, @Nullable String description) { - mChar = singleChar; - mText = text; - mIndex = index; - mOriginIndex = originIndex; - mDescription = description; - } - - public void insetNextEffect(Element element) { - Element origin = mNextEffect; - mNextEffect = element; - if (element != null) { - element.mNextEffect = origin; - } - } - - public void setPrev(Element element) { - this.mPrev = element; - if (element != null) { - element.mNext = this; - } - } - - public void setNext(Element element) { - this.mNext = element; - if (element != null) { - element.mPrev = this; - } - } - - public void setWordPart(int wordPart) { - this.mWordPart = wordPart; - } - - public int getWordPart() { - return mWordPart; - } - - public void setCanBreakWord(boolean canBreakWord) { - this.mCanBreakWord = canBreakWord; - } - - public boolean isCanBreakWord() { - return mCanBreakWord; - } - - void addSaveType(int type) { - if (mSaveType == null) { - mSaveType = new ArrayList<>(); - } - mSaveType.add(type); - } - - void removeSaveType(int type) { - if (mSaveType != null) { - for (int i = 0; i < mSaveType.size(); i++) { - if (mSaveType.get(i) == type) { - mSaveType.remove(i); - break; - } - } - } - } - - void addRestoreType(int type) { - if (mRestoreType == null) { - mRestoreType = new ArrayList<>(); - } - mRestoreType.add(type); - } - - void removeStoreType(int type) { - if (mRestoreType != null) { - for (int i = 0; i < mRestoreType.size(); i++) { - if (mRestoreType.get(i) == type) { - mRestoreType.remove(i); - break; - } - } - } - } - - public boolean hasEnvironmentUpdater() { - return mEnvironmentUpdater != null && mEnvironmentUpdater.size() > 0; - } - - void addEnvironmentUpdater(@NonNull EnvironmentUpdater environmentUpdater) { - if (mEnvironmentUpdater == null) { - mEnvironmentUpdater = new ArrayList<>(); - } - mEnvironmentUpdater.add(environmentUpdater); - } - - void removeEnvironmentUpdater(@NonNull EnvironmentUpdater environmentUpdater) { - if (mEnvironmentUpdater != null) { - mEnvironmentUpdater.remove(environmentUpdater); - } - } - - public void unsafeSingleEnvironmentUpdater(@Nullable List<Integer> changedTypes, @NonNull EnvironmentUpdater environmentUpdater) { - if (changedTypes != null) { - for (Integer type : changedTypes) { - addRestoreType(type); - addSaveType(type); - } - } - addEnvironmentUpdater(environmentUpdater); - } - - public void move(TypeEnvironment environment) { - if (hasEnvironmentUpdater()) { - updateEnv(environment); - restoreEnv(environment); - } - } - - public int getIndex() { - return mIndex; - } - - public int getOriginIndex() { - return mOriginIndex; - } - - public CharSequence getText() { - return mText; - } - - public char getChar() { - return mChar; - } - - public boolean isSingleChar() { - return mText == null; - } - - @NonNull - @Override - public String toString() { - return mText != null ? mText.toString() : String.valueOf(mChar); - } - - public int getLength() { - return mText != null ? mText.length() : 1; - } - - public String getDescription() { - return mDescription != null ? mDescription : toString(); - } - - protected void setMeasureDimen(float measureWidth, float measureHeight, float baseline) { - mMeasureWidth = measureWidth; - mMeasureHeight = measureHeight; - mBaseLine = baseline; - } - - public void setVisible(int visible) { - mVisible = visible; - } - - public int getVisible() { - return mVisible; - } - - public void setX(float x) { - mX = x; - } - - public void setY(float y) { - mY = y; - } - - public float getBaseLine() { - return mBaseLine; - } - - public float getMeasureWidth() { - return mMeasureWidth; - } - - public float getMeasureHeight() { - return mMeasureHeight; - } - - public float getX() { - return mX; - } - - public float getY() { - return mY; - } - - public Element getNext() { - return mNext; - } - - public Element getPrev() { - return mPrev; - } - - public void measure(TypeEnvironment env) { - updateEnv(env); - onMeasure(env); - restoreEnv(env); - } - - public void draw(TypeEnvironment env, Canvas canvas) { - updateEnv(env); - if (mVisible == VISIBLE) { - onDraw(env, canvas); - } - restoreEnv(env); - } - - void updateEnv(TypeEnvironment env) { - if (mSaveType != null) { - for (Integer type : mSaveType) { - env.save(type); - } - } - if (mEnvironmentUpdater != null) { - for (EnvironmentUpdater updater : mEnvironmentUpdater) { - updater.update(env); - } - } - } - - void restoreEnv(TypeEnvironment env) { - if (mRestoreType != null) { - for (Integer type : mRestoreType) { - env.restore(type); - } - } - } - - protected abstract void onMeasure(TypeEnvironment env); - - protected abstract void onDraw(TypeEnvironment env, Canvas canvas); -} diff --git a/type/src/main/java/com/qmuiteam/qmui/type/element/Element.kt b/type/src/main/java/com/qmuiteam/qmui/type/element/Element.kt new file mode 100644 index 000000000..00dafc797 --- /dev/null +++ b/type/src/main/java/com/qmuiteam/qmui/type/element/Element.kt @@ -0,0 +1,303 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.qmuiteam.qmui.type.element + +import android.graphics.Canvas +import android.graphics.Color +import com.qmuiteam.qmui.type.EnvironmentUpdater +import com.qmuiteam.qmui.type.TypeEnvironment +import java.util.* +import kotlin.collections.ArrayList + +abstract class Element(val text: CharSequence, val index: Int, val start: Int) { + companion object { + const val VISIBLE = 0 + const val GONE = 1 + const val WORD_PART_WHOLE = 0 + const val WORD_PART_START = 1 + const val WORD_PART_MIDDLE = 2 + const val WORD_PART_END = 3 + const val LINE_BREAK_TYPE_NORMAL = 0 + const val LINE_BREAK_TYPE_NOT_START = 1 + const val LINE_BREAK_TYPE_NOT_END = 2 + const val LINE_BREAK_WORD_BREAK_ALLOWED = 3 + private val NOT_START_CHARS = charArrayOf( + ',', '.', ';', ']', '>', ')', '?', '"', '\'', '!', ':', '}', '」', + ',', '。', ';', '、', '】', '》', ')', '?', '”', '!', ':', '』') + private val NOT_END_CHARS = charArrayOf( + '(', '<', '[', '{', '“', '「', '『', '(', '《' + ) + + init { + Arrays.sort(NOT_START_CHARS) + Arrays.sort(NOT_END_CHARS) + } + } + + var prevEffect: Element? = null + private set + var nextEffect: Element? = null + private set + + var wordPart = WORD_PART_WHOLE + var lineBreakType = LINE_BREAK_TYPE_NORMAL + var visible = VISIBLE + + + var measureWidth = 0 + private set + var measureHeight = 0 + private set + var x = 0 + var y = 0 + var baseLine = 0 + var nextGapWidth = 0 + + private var saveTypeList: MutableList<Int>? = null + private var restoreTypeList: MutableList<Int>? = null + private var environmentUpdaterList: MutableList<EnvironmentUpdater>? = null + + val length: Int = text.length + + private var _prev: Element? = null + private var _next: Element? = null + + var next: Element? + get() = _next + set(element) { + _next = element + if (element != null) { + element._prev = this + } + } + var prev: Element? + get() = _prev + set(element) { + _prev = element + if (element != null) { + element._next = this + } + } + + private val rightWithGap: Int + get() = x + measureWidth + nextGapWidth + + init { + if(text.length == 1){ + if (Arrays.binarySearch(NOT_START_CHARS, text[0]) >= 0) { + lineBreakType = LINE_BREAK_TYPE_NOT_START + } else if (Arrays.binarySearch(NOT_END_CHARS, text[0]) >= 0) { + lineBreakType = LINE_BREAK_TYPE_NOT_END + } + } + } + + fun insertEffectTo(head: Element): Element { + if (head === this) { + return head + } + if (index < head.index) { + head.prevEffect = this + nextEffect = head + return this + } + var current: Element = head + var next = head.nextEffect + while (next != null) { + if (next === this) { + // already in list + return head + } + if (next.index > index) { + current.nextEffect = this + next.prevEffect = this + prevEffect = current + nextEffect = next + return head + } + current = next + next = next.nextEffect + } + current.nextEffect = this + prevEffect = current + nextEffect = null + return head + } + + fun removeFromEffectListIfNeeded(head: Element?): Element? { + if(head == null){ + return null + } + val noSaveType = saveTypeList.isNullOrEmpty() + val noRestoreType = restoreTypeList.isNullOrEmpty() + if (noSaveType && noRestoreType) { + val prev = prevEffect + val next = nextEffect + if (prev != null) { + prev.nextEffect = next + prevEffect = null + } + if (next != null) { + next.prevEffect = prev + nextEffect = null + } + if (head === this) { + return next + } + } + return head + } + + fun addSaveType(type: Int) { + val list = saveTypeList ?: ArrayList<Int>().also { saveTypeList = it} + list.add(type) + } + + fun removeSaveType(type: Int) { + val list = saveTypeList ?: return + for (i in list.indices) { + if (list[i] == type) { + list.removeAt(i) + break + } + } + } + + fun addRestoreType(type: Int) { + val list = restoreTypeList ?: ArrayList<Int>().also { restoreTypeList = it} + list.add(type) + } + + fun removeStoreType(type: Int) { + val list = restoreTypeList ?: return + for (i in list.indices) { + if (list[i] == type) { + list.removeAt(i) + break + } + } + } + + fun hasEnvironmentUpdater(): Boolean { + return (environmentUpdaterList?.size ?: 0) > 0 + } + + fun hasSaveType(): Boolean { + return !saveTypeList.isNullOrEmpty() + } + + fun hasRestoreType(): Boolean { + return !restoreTypeList.isNullOrEmpty() + } + + fun addEnvironmentUpdater(environmentUpdater: EnvironmentUpdater) { + val list = environmentUpdaterList ?: ArrayList<EnvironmentUpdater>().also { environmentUpdaterList = it } + list.add(environmentUpdater) + } + + fun removeEnvironmentUpdater(environmentUpdater: EnvironmentUpdater) { + val list = environmentUpdaterList ?: return + list.remove(environmentUpdater) + } + + fun addSingleEnvironmentUpdater(changedTypes: List<Int>?, environmentUpdater: EnvironmentUpdater) { + if (changedTypes != null) { + for (type in changedTypes) { + addSaveType(type) + addRestoreType(type) + } + } + addEnvironmentUpdater(environmentUpdater) + } + + fun move(environment: TypeEnvironment) { + if (hasEnvironmentUpdater()) { + updateEnv(environment) + restoreEnv(environment) + } + } + + override fun toString(): String { + return text.toString() + } + + + protected fun setMeasureDimen(measureWidth: Int, measureHeight: Int, baseline: Int) { + this.measureWidth = measureWidth + this.measureHeight = measureHeight + baseLine = baseline + } + + + fun measure(env: TypeEnvironment) { + updateEnv(env) + onMeasure(env) + restoreEnv(env) + } + + fun draw(env: TypeEnvironment, canvas: Canvas) { + updateEnv(env) + if (visible == VISIBLE) { + onDraw(env, canvas) + } + restoreEnv(env) + } + + fun updateEnv(env: TypeEnvironment) { + saveTypeList?.forEach { + env.save(it) + } + environmentUpdaterList?.forEach { + it.update(env) + } + } + + fun restoreEnv(env: TypeEnvironment) { + restoreTypeList?.forEach { + env.restore(it) + } + } + + protected abstract fun onMeasure(env: TypeEnvironment) + protected abstract fun onDraw(env: TypeEnvironment, canvas: Canvas) + protected fun drawBg(env: TypeEnvironment, canvas: Canvas) { + if (env.backgroundColor != Color.TRANSPARENT) { + canvas.drawRect(x.toFloat(), y.toFloat(), rightWithGap.toFloat(), (y + measureHeight).toFloat(), env.bgPaint) + } + } + + protected fun drawBorder(env: TypeEnvironment, canvas: Canvas) { + val paint = env.borderPaint + if (env.borderLeftWidth > 0) { + paint.color = env.borderLeftColor + canvas.drawRect(x.toFloat(), y.toFloat(), (x + env.borderLeftWidth).toFloat(), (y + measureHeight).toFloat(), paint) + } + if (env.borderTopWidth > 0) { + paint.color = env.borderTopColor + canvas.drawRect(x.toFloat(), y.toFloat(), rightWithGap.toFloat(), (y + env.borderTopWidth).toFloat(), paint) + } + if (env.borderRightWidth > 0) { + paint.color = env.borderRightColor + canvas.drawRect((x + measureWidth - env.borderRightWidth).toFloat(), y.toFloat(), + (x + measureWidth).toFloat(), (y + measureHeight).toFloat(), paint) + } + if (env.borderBottomWidth > 0) { + paint.color = env.borderBottomColor + canvas.drawRect(x.toFloat(), (y + measureHeight - env.borderBottomWidth).toFloat(), + rightWithGap.toFloat(), (y + measureHeight).toFloat(), paint) + } + } +} \ No newline at end of file diff --git a/type/src/main/java/com/qmuiteam/qmui/type/element/EmojiElement.java b/type/src/main/java/com/qmuiteam/qmui/type/element/EmojiElement.java deleted file mode 100644 index 8ee7651d3..000000000 --- a/type/src/main/java/com/qmuiteam/qmui/type/element/EmojiElement.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.qmuiteam.qmui.type.element; - -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.drawable.Drawable; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.qmuiteam.qmui.type.TypeEnvironment; - -public class EmojiElement extends Element { - - private final Drawable mDrawable; - - public EmojiElement( - @NonNull Drawable drawable, - Character singleChar, - @Nullable CharSequence text, - int index, int originIndex) { - super(singleChar, text, index, originIndex, - text != null && text.length() > 2 && text.charAt(0) == '[' - ? text.subSequence(1, text.length() - 1).toString() : null); - mDrawable = drawable; - } - - @Override - protected void onMeasure(TypeEnvironment env) { - Paint paint = env.getPaint(); - int size = (int) (paint.getFontMetrics().descent - paint.getFontMetrics().ascent); - mDrawable.setBounds(0, 0, size, size); - setMeasureDimen(size, size, 0); - } - - @Override - protected void onDraw(TypeEnvironment env, Canvas canvas) { - canvas.save(); - canvas.translate(getX(), getY()); - mDrawable.draw(canvas); - canvas.restore(); - } -} diff --git a/type/src/main/java/com/qmuiteam/qmui/type/element/EmojiElement.kt b/type/src/main/java/com/qmuiteam/qmui/type/element/EmojiElement.kt new file mode 100644 index 000000000..4e4d77e39 --- /dev/null +++ b/type/src/main/java/com/qmuiteam/qmui/type/element/EmojiElement.kt @@ -0,0 +1,39 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.qmuiteam.qmui.type.element + +import android.graphics.Canvas +import android.graphics.drawable.Drawable +import com.qmuiteam.qmui.type.TypeEnvironment + +class EmojiElement(val drawable: Drawable, text: CharSequence, index: Int, start: Int) : Element(text, index, start) { + + override fun onMeasure(env: TypeEnvironment) { + val paint = env.paint + val size = (paint.fontMetrics.descent - paint.fontMetrics.ascent).toInt() + drawable.setBounds(0, 0, size, size) + setMeasureDimen(size, size, 0) + } + + override fun onDraw(env: TypeEnvironment, canvas: Canvas) { + drawBg(env, canvas) + canvas.save() + canvas.translate(x.toFloat(), y.toFloat()) + drawable.draw(canvas) + canvas.restore() + drawBorder(env, canvas) + } +} \ No newline at end of file diff --git a/skin-maker-plugin/src/test/java/com/qmuiteam/qmui/skinMaker/ExampleUnitTest.java b/type/src/main/java/com/qmuiteam/qmui/type/element/IgnoreEffectElement.kt similarity index 52% rename from skin-maker-plugin/src/test/java/com/qmuiteam/qmui/skinMaker/ExampleUnitTest.java rename to type/src/main/java/com/qmuiteam/qmui/type/element/IgnoreEffectElement.kt index 9b352a104..341bc85fc 100644 --- a/skin-maker-plugin/src/test/java/com/qmuiteam/qmui/skinMaker/ExampleUnitTest.java +++ b/type/src/main/java/com/qmuiteam/qmui/type/element/IgnoreEffectElement.kt @@ -13,21 +13,29 @@ * either express or implied. See the License for the specific language governing permissions and * limitations under the License. */ +package com.qmuiteam.qmui.type.element -package com.qmuiteam.qmui.skinMaker; +import android.graphics.Canvas +import com.qmuiteam.qmui.type.EnvironmentUpdater +import com.qmuiteam.qmui.type.TypeEnvironment -import org.junit.Test; +class IgnoreEffectElement(list: List<Element>) : Element("", -1, -1) { -import static org.junit.Assert.*; + init { + addEnvironmentUpdater(object: EnvironmentUpdater { + override fun update(env: TypeEnvironment) { + for (element in list) { + element.move(env) + } + } -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see <a href="http://d.android.com/tools/testing">Testing documentation</a> - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() { - assertEquals(4, 2 + 2); + }) + } + + override fun onMeasure(env: TypeEnvironment) { + setMeasureDimen(0, 0, 0) } + + override fun onDraw(env: TypeEnvironment, canvas: Canvas) {} + } \ No newline at end of file diff --git a/type/src/main/java/com/qmuiteam/qmui/type/element/NextParagraphElement.java b/type/src/main/java/com/qmuiteam/qmui/type/element/NextParagraphElement.java deleted file mode 100644 index 91daecc69..000000000 --- a/type/src/main/java/com/qmuiteam/qmui/type/element/NextParagraphElement.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, - * either express or implied. See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.qmuiteam.qmui.type.element; - -import android.graphics.Canvas; - -import androidx.annotation.Nullable; - -import com.qmuiteam.qmui.type.TypeEnvironment; - -public class NextParagraphElement extends Element { - - public NextParagraphElement(int index, int originIndex) { - this('\n', null, index, originIndex, "\n"); - } - - public NextParagraphElement(char singleChar, @Nullable String text, int index, int originIndex) { - super(singleChar, null, index, originIndex); - } - - public NextParagraphElement(char singleChar, @Nullable CharSequence text, int index, int originIndex, @Nullable String description) { - super(singleChar, text, index, originIndex, description); - } - - - - @Override - protected void onMeasure(TypeEnvironment env) { - setMeasureDimen(0, 0, 0); - } - - @Override - protected void onDraw(TypeEnvironment env, Canvas canvas) { - - } -} diff --git a/skin-maker/src/test/java/com/qmuiteam/qmui/skin/ExampleUnitTest.java b/type/src/main/java/com/qmuiteam/qmui/type/element/NextParagraphElement.kt similarity index 65% rename from skin-maker/src/test/java/com/qmuiteam/qmui/skin/ExampleUnitTest.java rename to type/src/main/java/com/qmuiteam/qmui/type/element/NextParagraphElement.kt index c178be12a..acaba752d 100644 --- a/skin-maker/src/test/java/com/qmuiteam/qmui/skin/ExampleUnitTest.java +++ b/type/src/main/java/com/qmuiteam/qmui/type/element/NextParagraphElement.kt @@ -13,21 +13,16 @@ * either express or implied. See the License for the specific language governing permissions and * limitations under the License. */ +package com.qmuiteam.qmui.type.element -package com.qmuiteam.qmui.skin; +import android.graphics.Canvas +import com.qmuiteam.qmui.type.TypeEnvironment -import org.junit.Test; +class NextParagraphElement(text: CharSequence, index: Int, start: Int) : Element(text, index, start) { -import static org.junit.Assert.*; - -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see <a href="http://d.android.com/tools/testing">Testing documentation</a> - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() { - assertEquals(4, 2 + 2); + override fun onMeasure(env: TypeEnvironment) { + setMeasureDimen(0, 0, 0) } + + override fun onDraw(env: TypeEnvironment, canvas: Canvas) {} } \ No newline at end of file diff --git a/type/src/main/java/com/qmuiteam/qmui/type/element/TextElement.kt b/type/src/main/java/com/qmuiteam/qmui/type/element/TextElement.kt new file mode 100644 index 000000000..02aa54c46 --- /dev/null +++ b/type/src/main/java/com/qmuiteam/qmui/type/element/TextElement.kt @@ -0,0 +1,37 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.qmuiteam.qmui.type.element + +import android.graphics.Canvas +import com.qmuiteam.qmui.type.TypeEnvironment + +open class TextElement(text: CharSequence, index: Int, start: Int) : Element(text, index, start) { + + override fun onMeasure(env: TypeEnvironment) { + val paint = env.paint + setMeasureDimen((paint.measureText(text, 0, text.length) + 0.5f).toInt(), + paint.fontMetricsInt.descent - paint.fontMetricsInt.ascent, + -paint.fontMetricsInt.ascent) + } + + override fun onDraw(env: TypeEnvironment, canvas: Canvas) { + drawBg(env, canvas) + canvas.drawText(text, 0, text.length, x.toFloat(), (y + baseLine).toFloat(), env.paint) + drawBorder(env, canvas) + } +} + +class MoreTextElement(text: CharSequence, index: Int, start: Int): TextElement(text, index, start) \ No newline at end of file diff --git a/type/src/main/java/com/qmuiteam/qmui/type/emoji/EmojiModel.kt b/type/src/main/java/com/qmuiteam/qmui/type/emoji/EmojiModel.kt new file mode 100644 index 000000000..996614e67 --- /dev/null +++ b/type/src/main/java/com/qmuiteam/qmui/type/emoji/EmojiModel.kt @@ -0,0 +1,177 @@ +package com.qmuiteam.qmui.type.emoji + +import android.text.SpannableString +import android.text.Spanned +import android.util.SparseArray +import androidx.core.util.putAll +import com.qmuiteam.qmui.type.TypeModel +import com.qmuiteam.qmui.type.element.Element +import com.qmuiteam.qmui.type.element.EmojiElement + +class Emoji(val span: EmojiSpan, val text: CharSequence, var start: Int) { + var prev: Emoji? = null + var next: Emoji? = null +} + +class EmojiModel( + val map: SparseArray<Emoji>, + var begin: Emoji, + var end: Emoji +) { + fun merge(other: EmojiModel) { + map.putAll(other.map) + when { + other.begin.start > end.start -> { + end.next = other.begin + other.begin.prev = end + end = other.end + } + other.end.start < begin.start -> { + other.end.next = begin + begin.prev = other.end + begin = other.begin + } + else -> { + var next: Emoji? + var otherNext: Emoji? + val newBegin: Emoji = if (begin.start < other.begin.start) { + next = begin.next + otherNext = other.begin + begin + } else { + otherNext = other.begin.next + next = begin + other.begin + } + var cur = newBegin + while (next != null || otherNext != null) { + when { + next == null -> { + cur.next = otherNext + otherNext!!.prev = cur + begin = newBegin + end = other.end + return + } + otherNext == null -> { + cur.next = next + next.prev = cur + begin = newBegin + return + } + next.start < otherNext.start -> { + cur.next = next + next.prev = cur + cur = next + next = next.next + } + else -> { + cur.next = otherNext + otherNext.prev = cur + cur = otherNext + otherNext = otherNext.next + } + } + } + } + } + } + + fun removeEmoji(emoji: Emoji): Boolean{ + map.remove(emoji.start) + if(emoji == begin){ + begin = emoji.next ?: return true + begin.prev = null + return false + } + + if(emoji == end){ + end = emoji.prev ?: return true + end.next = null + return false + } + val prev = emoji.prev + val next = emoji.next + prev?.next = next + next?.prev = prev + return false + } + + fun getEmoji(pos: Int): Emoji? { + if (begin.start > pos) { + return null + } + if (end.start + end.text.length <= pos) { + return null + } + var lo = 0 + var hi: Int = map.size() - 1 + + while (lo <= hi) { + val mid = lo + hi ushr 1 + val midVal = map.valueAt(mid) + when { + midVal.start + midVal.text.length <= pos -> { + lo = mid + 1 + } + midVal.start > pos -> { + hi = mid - 1 + } + else -> { + return midVal + } + } + } + return null + + } +} + +fun TypeModel.toEmojiModel(offset: Int, emojiSizeGetter: () -> Int): EmojiModel? { + var node: Element? = firstElement() + val map = SparseArray<Emoji>() + var begin: Emoji? = null + var end: Emoji? = null + while (node != null) { + if (node is EmojiElement) { + val next = Emoji(EmojiSpan(node.drawable.apply { + setBounds(0, 0, intrinsicWidth, intrinsicHeight) + }, emojiSizeGetter), node.text, offset + node.start) + map.put(offset + node.start, next) + if (begin == null) { + begin = next + end = next + } else { + next.prev = end + end!!.next = next + end = next + } + } + node = node.next + } + if (begin == null) { + return null + } + return EmojiModel(map, begin, end!!) +} + +fun TypeModel.toSpannableString(emojiSizeGetter: () -> Int): SpannableString{ + val ss = (origin as? SpannableString)?.apply { + getSpans(0, length, EmojiSpan::class.java)?.forEach { span -> + removeSpan(span) + } + } ?: SpannableString(origin) + var cur: Element? = firstElement() + while (cur != null){ + if(cur is EmojiElement){ + ss.setSpan( + EmojiSpan(cur.drawable, emojiSizeGetter), + cur.start, + cur.start + cur.text.length, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE + ) + } + cur = cur.next + } + return ss +} \ No newline at end of file diff --git a/type/src/main/java/com/qmuiteam/qmui/type/emoji/EmojiSpan.kt b/type/src/main/java/com/qmuiteam/qmui/type/emoji/EmojiSpan.kt new file mode 100644 index 000000000..35ec1f027 --- /dev/null +++ b/type/src/main/java/com/qmuiteam/qmui/type/emoji/EmojiSpan.kt @@ -0,0 +1,47 @@ +package com.qmuiteam.qmui.type.emoji + +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.drawable.Drawable +import android.text.style.ImageSpan +import com.qmuiteam.qmui.type.view.EmojiEditText + +class EmojiSpan(drawable: Drawable, val emojiSizeGetter: () -> Int) : ImageSpan(drawable) { + override fun getSize(paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int { + val emojiSize = emojiSizeGetter() + val rect = drawable.bounds + rect.left = 0 + rect.top = 0 + rect.right = if(emojiSize == EmojiEditText.EmojiOriginSize) drawable.intrinsicWidth else emojiSize + rect.bottom = if(emojiSize == EmojiEditText.EmojiOriginSize) drawable.intrinsicHeight else emojiSize + drawable.bounds = rect + if (fm != null) { + val fontMetricsHeight = fm.descent - fm.ascent + if(fontMetricsHeight < rect.bottom){ + val ratio = rect.bottom.toFloat() / fontMetricsHeight.toFloat() + fm.ascent = (fm.ascent * ratio).toInt() + fm.descent = (fm.descent * ratio).toInt() + fm.top = fm.ascent + fm.bottom = fm.descent + } + } + + return rect.right + } + + override fun draw( + canvas: Canvas, text: CharSequence?, start: Int, end: Int, + x: Float, top: Int, y: Int, bottom: Int, paint: Paint + ) { + canvas.save() + val rect = drawable.bounds + val fontMetricsInt = paint.fontMetricsInt + val fontTop = y + fontMetricsInt.top + val fontMetricsHeight = fontMetricsInt.bottom - fontMetricsInt.top + val iconHeight = rect.height() + val iconTop = fontTop + (fontMetricsHeight - iconHeight) / 2 + canvas.translate(x, iconTop.toFloat()) + drawable.draw(canvas) + canvas.restore() + } +} \ No newline at end of file diff --git a/type/src/main/java/com/qmuiteam/qmui/type/parser/EmojiResourceProvider.java b/type/src/main/java/com/qmuiteam/qmui/type/parser/EmojiResourceProvider.java deleted file mode 100644 index 8c736e21f..000000000 --- a/type/src/main/java/com/qmuiteam/qmui/type/parser/EmojiResourceProvider.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, - * either express or implied. See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.qmuiteam.qmui.type.parser; - -import android.graphics.drawable.Drawable; - -import androidx.annotation.Nullable; - -public interface EmojiResourceProvider { - - @Nullable - Drawable queryForDrawable(CharSequence text); - - @Nullable - Drawable queryForDrawable(char c); - - @Nullable - Drawable queryForDrawable(int codePoint); - - @Nullable - Drawable queryForDrawable(int firstCodePoint, int secondCodePint); - - -} diff --git a/qmui/src/main/java/com/qmuiteam/qmui/util/DoNotInterceptKeyboardInset.java b/type/src/main/java/com/qmuiteam/qmui/type/parser/EmojiResourceProvider.kt similarity index 67% rename from qmui/src/main/java/com/qmuiteam/qmui/util/DoNotInterceptKeyboardInset.java rename to type/src/main/java/com/qmuiteam/qmui/type/parser/EmojiResourceProvider.kt index 5221e6d4d..7ea6dc5da 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/util/DoNotInterceptKeyboardInset.java +++ b/type/src/main/java/com/qmuiteam/qmui/type/parser/EmojiResourceProvider.kt @@ -13,15 +13,13 @@ * either express or implied. See the License for the specific language governing permissions and * limitations under the License. */ +package com.qmuiteam.qmui.type.parser -package com.qmuiteam.qmui.util; +import android.graphics.drawable.Drawable -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 DoNotInterceptKeyboardInset { -} +interface EmojiResourceProvider { + fun queryForDrawable(text: CharSequence): Drawable? + fun queryForDrawable(c: Char): Drawable? + fun queryForDrawable(codePoint: Int): Drawable? + fun queryForDrawable(firstCodePoint: Int, secondCodePint: Int): Drawable? +} \ No newline at end of file diff --git a/type/src/main/java/com/qmuiteam/qmui/type/parser/EmojiTextParser.java b/type/src/main/java/com/qmuiteam/qmui/type/parser/EmojiTextParser.java deleted file mode 100644 index c4cb6bd52..000000000 --- a/type/src/main/java/com/qmuiteam/qmui/type/parser/EmojiTextParser.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, - * either express or implied. See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.qmuiteam.qmui.type.parser; - -import android.graphics.drawable.Drawable; - -import com.qmuiteam.qmui.type.TypeModel; -import com.qmuiteam.qmui.type.element.CharOrPhraseElement; -import com.qmuiteam.qmui.type.element.DrawableElement; -import com.qmuiteam.qmui.type.element.Element; -import com.qmuiteam.qmui.type.element.EmojiElement; -import com.qmuiteam.qmui.type.element.NextParagraphElement; - -import java.util.HashMap; - -public class EmojiTextParser implements TextParser { - - private final EmojiResourceProvider mEmojiProvider; - - public EmojiTextParser(EmojiResourceProvider provider) { - mEmojiProvider = provider; - } - - @Override - public TypeModel parse(CharSequence text) { - int size = text.length(); - if (size == 0) { - return null; - } - HashMap<Integer, Element> map = new HashMap<>(text.length()); - Element first = null, last = null, tmp = null; - int index = 0; - for (int i = 0; i < size; i++) { - char c = text.charAt(i); - if (c == '\n') { - tmp = new NextParagraphElement(c, null, index, i); - } else if (c == '\r') { - if (i + 1 < text.length() && text.charAt(i + 1) == '\n') { - tmp = new NextParagraphElement('\u0000', "\r\n", index, i); - i++; - } else { - tmp = new NextParagraphElement(c, null, index, i); - } - } else if (c == '[') { - int j = i + 1; - boolean find = false; - int end = Math.min(i + 30, size); - while (j < end) { - if (text.charAt(j) == ']') { - CharSequence sub = text.subSequence(i, j + 1); - Drawable emoji = mEmojiProvider.queryForDrawable(sub); - if (emoji != null) { - tmp = new EmojiElement(emoji, '\u0000', sub, index, i); - i = j; - find = true; - break; - } - } - j++; - } - if (!find) { - tmp = new CharOrPhraseElement(c, index, i); - } - } else { - boolean handled = false; - Drawable emoji = mEmojiProvider.queryForDrawable(c); - if (emoji != null) { - handled = true; - tmp = new DrawableElement(emoji, c, null, index, i); - } - - if (!handled) { - int unicode = Character.codePointAt(text, i); - int codeCount = Character.charCount(unicode); - emoji = mEmojiProvider.queryForDrawable(unicode); - if (emoji != null) { - handled = true; - tmp = new DrawableElement(emoji, c, text.subSequence(i, i + codeCount), index, i); - i += codeCount - 1; - } - - int nextStart = i + codeCount; - if (!handled && nextStart < size) { - int nextUnicode = Character.codePointAt(text, nextStart); - emoji = mEmojiProvider.queryForDrawable(unicode, nextUnicode); - if (emoji != null) { - handled = true; - int nextCodeCount = Character.charCount(nextUnicode); - tmp = new DrawableElement(emoji, c, text.subSequence(i, nextStart + nextCodeCount), index, i); - i = nextStart + nextCodeCount - 1; - } - } - } - - if (!handled) { - tmp = new CharOrPhraseElement(c, index, i); - } - } - - ParserHelper.handleWordPart(c, last, tmp); - - index++; - if (first == null) { - first = tmp; - last = tmp; - } else { - last.setNext(tmp); - last = tmp; - } - map.put(tmp.getIndex(), tmp); - } - return new TypeModel(map, first, last, null); - } -} diff --git a/type/src/main/java/com/qmuiteam/qmui/type/parser/EmojiTextParser.kt b/type/src/main/java/com/qmuiteam/qmui/type/parser/EmojiTextParser.kt new file mode 100644 index 000000000..68e31c1d7 --- /dev/null +++ b/type/src/main/java/com/qmuiteam/qmui/type/parser/EmojiTextParser.kt @@ -0,0 +1,124 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.qmuiteam.qmui.type.parser + +import com.qmuiteam.qmui.type.TypeModel +import com.qmuiteam.qmui.type.element.Element +import com.qmuiteam.qmui.type.element.EmojiElement +import com.qmuiteam.qmui.type.element.NextParagraphElement +import com.qmuiteam.qmui.type.element.TextElement +import java.util.* + +class EmojiTextParser( + private val emojiProvider: EmojiResourceProvider, + private val wordBreakChecker: (c: Char) -> Boolean +) : TextParser { + + override fun parse(text: CharSequence?): TypeModel? { + if(text == null || text.isEmpty()){ + return null + } + + val size = text.length + val map = HashMap<Int, Element>(size) + var first: Element? = null + var last: Element? = null + var tmp: Element? = null + var index = 0 + var i = 0 + while (i < size) { + val c = text[i] + if (c == '\n') { + tmp = NextParagraphElement(text.subSequence(i, i + 1), index, i) + } else if (c == '\r') { + if (i + 1 < text.length && text[i + 1] == '\n') { + tmp = NextParagraphElement(text.subSequence(i, i + 2), index, i) + i++ + } else { + tmp = NextParagraphElement(text.subSequence(i, i + 1), index, i) + } + } else if (c == '[') { + var j = i + 1 + var find = false + val end = Math.min(i + 30, size) + while (j < end) { + if (text[j] == ']') { + val sub = text.subSequence(i, j + 1) + val emoji = emojiProvider.queryForDrawable(sub) + if (emoji != null) { + tmp = EmojiElement(emoji, text.subSequence(i, j + 1), index, i) + i = j + find = true + break + } + } + j++ + } + if (!find) { + val unicode = Character.codePointAt(text, i) + val charCount = Character.charCount(unicode) + tmp = TextElement(text.subSequence(i, i + charCount), index, i) + i += charCount - 1 + } + } else { + var handled = false + var emoji = emojiProvider.queryForDrawable(c) + if (emoji != null) { + handled = true + tmp = EmojiElement(emoji, text.subSequence(i, i + 1), index, i) + } + if (!handled) { + val unicode = Character.codePointAt(text, i) + val codeCount = Character.charCount(unicode) + emoji = emojiProvider.queryForDrawable(unicode) + if (emoji != null) { + handled = true + tmp = EmojiElement(emoji, text.subSequence(i, i + codeCount), index, i) + i += codeCount - 1 + } + val nextStart = i + codeCount + if (!handled && nextStart < size) { + val nextUnicode = Character.codePointAt(text, nextStart) + emoji = emojiProvider.queryForDrawable(unicode, nextUnicode) + if (emoji != null) { + handled = true + val nextCodeCount = Character.charCount(nextUnicode) + tmp = EmojiElement(emoji, text.subSequence(i, nextStart + nextCodeCount), index, i) + i = nextStart + nextCodeCount - 1 + } + } + } + if (!handled) { + val charCount = ParserHelper.handleUnionIfNeeded(text, i) + tmp = TextElement(text.subSequence(i, i + charCount), index, i) + i += charCount - 1 + } + } + ParserHelper.handleWordPart(c, last, tmp!!, wordBreakChecker) + index++ + if (first == null) { + first = tmp + last = tmp + } else { + last!!.next = tmp + last = tmp + } + map[tmp.index] = tmp + i++ + } + return TypeModel(text, map, first!!, last!!) + } +} \ No newline at end of file diff --git a/type/src/main/java/com/qmuiteam/qmui/type/parser/ParserHelper.java b/type/src/main/java/com/qmuiteam/qmui/type/parser/ParserHelper.java deleted file mode 100644 index a76521a71..000000000 --- a/type/src/main/java/com/qmuiteam/qmui/type/parser/ParserHelper.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, - * either express or implied. See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.qmuiteam.qmui.type.parser; - -import com.qmuiteam.qmui.type.element.Element; - -public class ParserHelper { - - public static boolean isEnglishLetterOrNumber(char c){ - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'); - } - - public static void handleWordPart(char c, Element prev, Element curr){ - if(ParserHelper.isEnglishLetterOrNumber(c)){ - if(prev == null || - prev.getWordPart() == Element.WORD_PART_WHOLE || - prev.getWordPart() == Element.WORD_PART_END){ - curr.setWordPart(Element.WORD_PART_START); - }else{ - curr.setWordPart(Element.WORD_PART_MIDDLE); - } - }else{ - if(prev != null && prev.getWordPart() == Element.WORD_PART_MIDDLE){ - prev.setWordPart(Element.WORD_PART_END); - } - curr.setWordPart(Element.WORD_PART_WHOLE); - } - } -} diff --git a/type/src/main/java/com/qmuiteam/qmui/type/parser/ParserHelper.kt b/type/src/main/java/com/qmuiteam/qmui/type/parser/ParserHelper.kt new file mode 100644 index 000000000..01f33ce5d --- /dev/null +++ b/type/src/main/java/com/qmuiteam/qmui/type/parser/ParserHelper.kt @@ -0,0 +1,62 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.qmuiteam.qmui.type.parser + +import com.qmuiteam.qmui.type.element.Element + +object ParserHelper { + + fun isEnglishLetterOrNumber(c: Char): Boolean { + return c in 'a'..'z' || c in 'A'..'Z' || c in '0'..'9' + } + + fun handleWordPart(c: Char, prev: Element?, curr: Element, wordBreakChecker: (c: Char) -> Boolean) { + if (isEnglishLetterOrNumber(c)) { + if (prev == null || prev.wordPart == Element.WORD_PART_WHOLE || prev.wordPart == Element.WORD_PART_END) { + curr.wordPart = Element.WORD_PART_START + } else { + curr.wordPart = Element.WORD_PART_MIDDLE + if (wordBreakChecker(c)) { + curr.lineBreakType = Element.LINE_BREAK_WORD_BREAK_ALLOWED + } + } + } else { + if (prev != null && prev.wordPart == Element.WORD_PART_MIDDLE) { + prev.wordPart = Element.WORD_PART_END + prev.lineBreakType = Element.LINE_BREAK_TYPE_NORMAL + } + curr.wordPart = Element.WORD_PART_WHOLE + } + } + + fun handleUnionIfNeeded(text: CharSequence, i: Int): Int { + val unicode = Character.codePointAt(text, i) + var charCount = Character.charCount(unicode) + var next = i + charCount + while (next < text.length) { + val nextUnicode = Character.codePointAt(text, next) + val type = Character.getType(nextUnicode) + if (type == Character.NON_SPACING_MARK.toInt()) { + val nextCharCount = Character.charCount(nextUnicode) + charCount += nextCharCount + next += nextCharCount + } else { + break + } + } + return charCount + } +} \ No newline at end of file diff --git a/type/src/main/java/com/qmuiteam/qmui/type/parser/PlainTextParser.java b/type/src/main/java/com/qmuiteam/qmui/type/parser/PlainTextParser.java deleted file mode 100644 index 8a5dab98f..000000000 --- a/type/src/main/java/com/qmuiteam/qmui/type/parser/PlainTextParser.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, - * either express or implied. See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.qmuiteam.qmui.type.parser; - -import com.qmuiteam.qmui.type.TypeModel; -import com.qmuiteam.qmui.type.element.CharOrPhraseElement; -import com.qmuiteam.qmui.type.element.Element; -import com.qmuiteam.qmui.type.element.NextParagraphElement; - -import java.util.HashMap; - -public class PlainTextParser implements TextParser { - @Override - public TypeModel parse(CharSequence text) { - if (text.length() == 0) { - return null; - } - HashMap<Integer, Element> map = new HashMap<>(text.length()); - Element first = null, last = null, tmp; - int index = 0; - for (int i = 0; i < text.length(); i++) { - char c = text.charAt(i); - if (c == '\n') { - tmp = new NextParagraphElement(c, null, index, i); - } else if (c == '\r') { - if (i + 1 < text.length() && text.charAt(i + 1) == '\n') { - tmp = new NextParagraphElement('\u0000', "\r\n", index, i); - i++; - } else { - tmp = new NextParagraphElement(c, null, index, i); - } - } else { - tmp = new CharOrPhraseElement(c, index, i); - } - - ParserHelper.handleWordPart(c, last, tmp); - - index++; - if (first == null) { - first = tmp; - last = tmp; - } else { - last.setNext(tmp); - last = tmp; - } - map.put(tmp.getIndex(), tmp); - } - return new TypeModel(map, first, last, null); - } -} diff --git a/type/src/main/java/com/qmuiteam/qmui/type/parser/PlainTextParser.kt b/type/src/main/java/com/qmuiteam/qmui/type/parser/PlainTextParser.kt new file mode 100644 index 000000000..bf4bade75 --- /dev/null +++ b/type/src/main/java/com/qmuiteam/qmui/type/parser/PlainTextParser.kt @@ -0,0 +1,78 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.qmuiteam.qmui.type.parser + +import com.qmuiteam.qmui.type.TypeModel +import com.qmuiteam.qmui.type.element.Element +import com.qmuiteam.qmui.type.element.NextParagraphElement +import com.qmuiteam.qmui.type.element.TextElement +import com.qmuiteam.qmui.type.parser.ParserHelper.handleWordPart +import java.util.* + +class PlainTextParser( + private val wordBreakChecker: (c: Char) -> Boolean +) : TextParser { + + companion object { + val instance by lazy { + PlainTextParser { + false + } + } + } + + override fun parse(text: CharSequence?): TypeModel? { + if (text == null || text.isEmpty()) { + return null + } + val size = text.length + val map = HashMap<Int, Element>(size) + var first: Element? = null + var last: Element? = null + var tmp: Element + var index = 0 + var i = 0 + while (i < size) { + val c = text[i] + if (c == '\n') { + tmp = NextParagraphElement(text.subSequence(i, i + 1), index, i) + } else if (c == '\r') { + if (i + 1 < text.length && text[i + 1] == '\n') { + tmp = NextParagraphElement(text.subSequence(i, i + 2), index, i) + i++ + } else { + tmp = NextParagraphElement(text.subSequence(i, i + 1), index, i) + } + } else { + val charCount = ParserHelper.handleUnionIfNeeded(text, i) + tmp = TextElement(text.subSequence(i, i + charCount), index, i) + i += charCount - 1 + } + handleWordPart(c, last, tmp, wordBreakChecker) + index++ + if (first == null) { + first = tmp + last = tmp + } else { + last!!.next = tmp + last = tmp + } + map[tmp.index] = tmp + i++ + } + return TypeModel(text, map, first!!, last!!) + } +} \ No newline at end of file diff --git a/type/src/main/java/com/qmuiteam/qmui/type/parser/TextParser.java b/type/src/main/java/com/qmuiteam/qmui/type/parser/TextParser.kt similarity index 82% rename from type/src/main/java/com/qmuiteam/qmui/type/parser/TextParser.java rename to type/src/main/java/com/qmuiteam/qmui/type/parser/TextParser.kt index 61167d59d..89c7eec15 100644 --- a/type/src/main/java/com/qmuiteam/qmui/type/parser/TextParser.java +++ b/type/src/main/java/com/qmuiteam/qmui/type/parser/TextParser.kt @@ -13,11 +13,10 @@ * either express or implied. See the License for the specific language governing permissions and * limitations under the License. */ +package com.qmuiteam.qmui.type.parser -package com.qmuiteam.qmui.type.parser; +import com.qmuiteam.qmui.type.TypeModel -import com.qmuiteam.qmui.type.TypeModel; - -public interface TextParser { - TypeModel parse(CharSequence text); -} +interface TextParser { + fun parse(text: CharSequence?): TypeModel? +} \ No newline at end of file diff --git a/type/src/main/java/com/qmuiteam/qmui/type/view/BaseTypeView.kt b/type/src/main/java/com/qmuiteam/qmui/type/view/BaseTypeView.kt new file mode 100644 index 000000000..9f4d8d35d --- /dev/null +++ b/type/src/main/java/com/qmuiteam/qmui/type/view/BaseTypeView.kt @@ -0,0 +1,96 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.qmuiteam.qmui.type.view + +import android.content.Context +import android.graphics.Typeface +import android.util.AttributeSet +import android.view.View +import com.qmuiteam.qmui.type.TypeEnvironment + +open class BaseTypeView(context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0): View(context, attrs, defStyleAttr) { + val environment = TypeEnvironment() + + var textSize: Float + get() = environment.textSize + set(value){ + throwIfRunning("setTextSize") + if(environment.textSize != value){ + environment.textSize = value + requestLayout() + } + } + + var textColor: Int + get() = environment.textColor + set(value){ + throwIfRunning("setTextColor") + if(environment.textColor != value){ + environment.textColor = value + invalidate() + } + } + + var typeface: Typeface? + get() = environment.typeface + set(value){ + throwIfRunning("setTypeface") + if(environment.typeface != value){ + environment.typeface = value + requestLayout() + } + } + + var lineSpace: Int + get() = environment.lineSpace + set(value){ + throwIfRunning("setLineSpace") + if(environment.lineSpace != value){ + environment.lineSpace = value + requestLayout() + } + } + + var lineHeight: Int + get() = environment.lineHeight + set(value){ + throwIfRunning("setLineHeight") + if(environment.lineHeight != value){ + environment.lineHeight = value + requestLayout() + } + } + + var paragraphSpace: Int + get() = environment.paragraphSpace + set(value){ + throwIfRunning("setParagraphSpace") + if(environment.paragraphSpace != value){ + environment.paragraphSpace = value + requestLayout() + } + } + + + fun throwIfRunning(action: String) { + if(environment.isRunning()){ + throw RuntimeException("can not perform $action when running.") + } + } +} \ No newline at end of file diff --git a/type/src/main/java/com/qmuiteam/qmui/type/view/EmojiEditText.kt b/type/src/main/java/com/qmuiteam/qmui/type/view/EmojiEditText.kt new file mode 100644 index 000000000..ead97e98a --- /dev/null +++ b/type/src/main/java/com/qmuiteam/qmui/type/view/EmojiEditText.kt @@ -0,0 +1,245 @@ +package com.qmuiteam.qmui.type.view + +import android.content.Context +import android.text.Editable +import android.text.Spannable +import android.text.SpannableString +import android.text.TextWatcher +import android.util.AttributeSet +import android.util.Log +import androidx.appcompat.widget.AppCompatEditText +import androidx.core.util.valueIterator +import com.qmuiteam.qmui.type.emoji.Emoji +import com.qmuiteam.qmui.type.emoji.EmojiModel +import com.qmuiteam.qmui.type.emoji.EmojiSpan +import com.qmuiteam.qmui.type.emoji.toEmojiModel +import com.qmuiteam.qmui.type.parser.EmojiTextParser + +open class EmojiEditText( + context: Context, + attributeSet: AttributeSet? = null +) : AppCompatEditText(context, attributeSet) { + + companion object { + const val EmojiOriginSize = -1 + } + + + private var emojiModel: EmojiModel? = null + private var isTextFirstSet: Boolean = false + private var isTextManualSetting: Boolean = false + + var emojiSize: Int = EmojiOriginSize + set(value) { + if (field != value) { + field = value + invalidate() + } + } + + var textParser: EmojiTextParser? = null + set(value) { + if (field != value) { + field = value + if (isTextFirstSet) { + setText(text, BufferType.EDITABLE) + } + } + } + + private val textWatcher = object : TextWatcher { + + private val pendingRemoveRange = mutableListOf<Pair<Int, Int>>() + private var isPendingRemoving = false + private val correctingEmoji = ArrayList<Emoji>() + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + if (isPendingRemoving || isTextManualSetting) { + return + } + val model = emojiModel + if (s != null && model != null && count > 0) { + val end = start + count + var current = start + while (current < end) { + val emoji = model.getEmoji(current) + if (emoji != null) { + var shouldBreak = false + if (model.removeEmoji(emoji)) { + emojiModel = null + shouldBreak = true + } + if (emoji.start < start) { + pendingRemoveRange.add(emoji.start to start) + } + val emojiEnd = emoji.start + emoji.text.length + if (emojiEnd > end) { + val offset = count - after + // remove first. + pendingRemoveRange.add(0, (end - offset) to (emojiEnd - offset)) + } + if (shouldBreak) { + break; + } + current = emoji.start + emoji.text.length + } else { + current++ + } + } + } + } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + if (isTextManualSetting) { + return + } + + val offset = count - before + emojiModel?.map?.let { map -> + map.valueIterator().asSequence().mapTo(correctingEmoji) { emoji -> + if (emoji.start >= start) { + emoji.start += offset + } + emoji + } + map.clear() + correctingEmoji.forEach { + map.put(it.start, it) + } + correctingEmoji.clear() + } + + if (isPendingRemoving) { + return + } + + if (count > 0) { + val insertModel = + textParser?.parse(s!!.subSequence(start, start + count))?.toEmojiModel(start){ emojiSize } + if (insertModel != null) { + val model = emojiModel + if (model != null) { + model.merge(insertModel) + } else { + emojiModel = insertModel + } + } + } + + } + + override fun afterTextChanged(s: Editable?) { + if (isPendingRemoving || isTextManualSetting) { + return + } + isPendingRemoving = true + pendingRemoveRange.forEach { + s?.delete(it.first, it.second) + } + pendingRemoveRange.clear() + s?.getSpans(0, s.length, EmojiSpan::class.java)?.forEach { + s.removeSpan(it) + } + emojiModel?.map?.valueIterator()?.forEach { + s?.setSpan( + it.span, + it.start, + it.start + it.text.length, + Spannable.SPAN_INCLUSIVE_EXCLUSIVE + ) + } + + fixSelection() + isPendingRemoving = false + } + + } + + init { + super.addTextChangedListener(textWatcher) + } + + fun replaceSelection(toInsert: CharSequence) { + val origin = text + if (origin == null) { + setText(toInsert) + } else { + if (selectionStart < 0 || selectionEnd < 0) { + setSelection(origin.length, origin.length) + } + if (selectionStart == selectionEnd) { + origin.insert(selectionEnd, toInsert) + } else { + var fixStart = selectionStart + emojiModel?.getEmoji(selectionStart)?.let { + val end = it.start + it.text.length + if (selectionStart > it.start && selectionStart == end - 1) { + fixStart = end + } + } + origin.replace(fixStart, selectionEnd, toInsert) + } + } + } + + fun delete() { + val origin = text ?: return + if (selectionStart != selectionEnd) { + origin.replace(selectionStart, selectionEnd, "") + } else if (selectionStart > 0) { + origin.delete(selectionStart - 1, selectionEnd) + } + } + + override fun setText(text: CharSequence?, type: BufferType?) { + isTextFirstSet = true + val model = textParser?.parse(text)?.toEmojiModel(0) { emojiSize } + emojiModel = model + val spannable = if (text is Spannable) { + val spans = text.getSpans(0, text.length, EmojiSpan::class.java) + for (span in spans) { + text.removeSpan(span) + } + text + } else { + SpannableString(text ?: "") + } + model?.map?.valueIterator()?.forEach { + spannable.setSpan( + it.span, + it.start, + it.start + it.text.length, + Spannable.SPAN_INCLUSIVE_EXCLUSIVE + ) + } + isTextManualSetting = true + super.setText(spannable, type) + fixSelection() + isTextManualSetting = false + } + + private fun fixSelection() { + val model = emojiModel ?: return + if (selectionStart == selectionEnd) { + val emoji = model.getEmoji(selectionStart) ?: return + if (selectionStart > emoji.start && selectionStart < (emoji.start + emoji.text.length)) { + setSelection(emoji.start + emoji.text.length) + } + } else { + var fixStart = selectionStart + var fixEnd = selectionEnd + val start = model.getEmoji(selectionStart) + if (start != null && selectionStart > start.start && + selectionStart < (start.start + start.text.length) + ) { + fixStart = start.start + } + val end = model.getEmoji(selectionEnd) + if (end != null && selectionEnd > end.start && selectionEnd < (end.start + end.text.length)) { + fixEnd = end.start + end.text.length + } + setSelection(fixStart, fixEnd) + } + + } +} \ No newline at end of file diff --git a/type/src/main/java/com/qmuiteam/qmui/type/view/LineTypeView.kt b/type/src/main/java/com/qmuiteam/qmui/type/view/LineTypeView.kt new file mode 100644 index 000000000..5db620c5b --- /dev/null +++ b/type/src/main/java/com/qmuiteam/qmui/type/view/LineTypeView.kt @@ -0,0 +1,275 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.qmuiteam.qmui.type.view + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Typeface +import android.text.TextUtils +import android.util.AttributeSet +import android.util.Log +import android.view.MotionEvent +import android.view.accessibility.AccessibilityNodeInfo +import androidx.annotation.ColorInt +import com.qmuiteam.qmui.type.LineLayout +import com.qmuiteam.qmui.type.TypeEnvironment +import com.qmuiteam.qmui.type.TypeModel +import com.qmuiteam.qmui.type.parser.PlainTextParser +import com.qmuiteam.qmui.type.parser.TextParser +import java.util.* + +private const val TAG = "LineTypeView" + +open class LineTypeView : BaseTypeView { + + val lineLayout = LineLayout() + + var textParser: TextParser = PlainTextParser.instance + set(value) { + if (field != value) { + field = value + lineLayout.typeModel = value.parse(text) + requestLayout() + } + } + + private val touchSpanList = arrayListOf<TouchSpan>() + private var currentTouchSpan: TouchSpan? = null + + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val widthSize = MeasureSpec.getSize(widthMeasureSpec) + val widthMode = MeasureSpec.getMode(widthMeasureSpec) + val heightSize = MeasureSpec.getSize(heightMeasureSpec) + val heightMode = MeasureSpec.getMode(heightMeasureSpec) + environment.setMeasureLimit(widthSize - paddingLeft - paddingRight, heightSize - paddingTop - paddingBottom) + lineLayout.measureAndLayout(environment, heightMode == MeasureSpec.EXACTLY) + val usedWidth = if (widthMode == MeasureSpec.AT_MOST) { + lineLayout.maxLayoutWidth + paddingLeft + paddingRight + } else widthSize + val usedHeight = if (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED) { + lineLayout.contentHeight + paddingTop + paddingBottom + } else heightSize + setMeasuredDimension(usedWidth, usedHeight) + } + + var text: CharSequence? = null + set(value) { + if (field != value) { + field = value + touchSpanList.clear() + currentTouchSpan = null + lineLayout.typeModel = textParser.parse(value) + requestLayout() + } + } + + var ellipsized: TextUtils.TruncateAt? + get() = lineLayout.ellipsize + set(value) { + if (lineLayout.ellipsize != value) { + lineLayout.ellipsize = value + requestLayout() + } + } + + var maxLines: Int + get() = lineLayout.maxLines + set(value) { + if (lineLayout.maxLines != value) { + lineLayout.maxLines = value + requestLayout() + } + } + + override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) { + super.onInitializeAccessibilityNodeInfo(info) + info.text = text + info.contentDescription = text + } + + fun addClickEffect( + start: Int, end: Int, + textColorGetter: (isPressed: Boolean) -> Int, + bgColorGetter: (isPressed: Boolean) -> Int, + onClick: (start: Int, end: Int) -> Unit + ): TypeModel.EffectRemover? { + val types: MutableList<Int> = ArrayList() + types.add(TypeEnvironment.TYPE_BG_COLOR) + types.add(TypeEnvironment.TYPE_TEXT_COLOR) + return unsafeAddClickEffect(start, end, types, { env, touchSpan -> + env.textColor = textColorGetter.invoke(touchSpan.isPressed) + env.backgroundColor = bgColorGetter.invoke(touchSpan.isPressed) + }, onClick) + } + + fun unsafeAddClickEffect( + start: Int, end: Int, + types: List<Int>, updater: (TypeEnvironment, touchSpan: TouchSpan) -> Unit, onClick: (Int, Int) -> Unit + ): TypeModel.EffectRemover? { + val typeModel = lineLayout.typeModel ?: return null + val touchSpan = TouchSpan(start, end, onClick) + val remover = typeModel.unsafeAddEffect(start, end, types) { + updater.invoke(it, touchSpan) + } + touchSpanList.add(touchSpan) + return TypeModel.EffectRemover { + remover?.remove() + touchSpanList.remove(touchSpan) + } + } + + fun addBgEffect(start: Int, end: Int, @ColorInt color: Int): TypeModel.EffectRemover? { + val typeModel = lineLayout.typeModel ?: return null + val remover = typeModel.addBgEffect(start, end, color) ?: return null + invalidate() + return remover + } + + fun addTextColorEffect(start: Int, end: Int, @ColorInt color: Int): TypeModel.EffectRemover? { + val typeModel = lineLayout.typeModel ?: return null + val remover = typeModel.addTextColorEffect(start, end, color) ?: return null + invalidate() + return remover + } + + fun addUnderLineEffect(start: Int, end: Int, @ColorInt color: Int, height: Int): TypeModel.EffectRemover? { + val typeModel = lineLayout.typeModel ?: return null + val remover = typeModel.addUnderLineEffect(start, end, color, height) ?: return null + invalidate() + return remover + } + + fun addTypefaceEffect(start: Int, end: Int, typeface: Typeface): TypeModel.EffectRemover? { + val typeModel = lineLayout.typeModel ?: return null + val remover = typeModel.addTypefaceEffect(start, end, typeface) ?: return null + requestLayout() + return remover + } + + fun addTextSizeEffect(start: Int, end: Int, textSize: Float): TypeModel.EffectRemover? { + val typeModel = lineLayout.typeModel ?: return null + val remover = typeModel.addTextSizeEffect(start, end, textSize) ?: return null + requestLayout() + return remover + } + + override fun onDraw(canvas: Canvas) { + canvas.save() + canvas.translate(paddingLeft.toFloat(), paddingTop.toFloat()) + lineLayout.draw(canvas, environment) + canvas.restore() + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + val typeModel = lineLayout.typeModel ?: return super.onTouchEvent(event) + if (touchSpanList.isEmpty()) { + return super.onTouchEvent(event) + } + when (event.action) { + MotionEvent.ACTION_DOWN -> { + val current = currentTouchSpan + if (current != null) { + Log.i(TAG, "the currentTouchSpan is not null when touch down.") + current.isPressed = false + } + val touchSpan = findCurrentTouchSpan(typeModel, event.x, event.y) + if (touchSpan != null) { + touchSpan.isPressed = true + currentTouchSpan = touchSpan + invalidate() + return true + } + } + MotionEvent.ACTION_MOVE -> { + val current = currentTouchSpan + if (current != null) { + if (!isSpanTouched(typeModel, current, event.x, event.y)) { + current.isPressed = false + val touchSpan = findCurrentTouchSpan(typeModel, event.x, event.y) + if (touchSpan != null) { + touchSpan.isPressed = true + currentTouchSpan = touchSpan + } else { + currentTouchSpan = null + } + invalidate() + } + return true + } else { + val touchSpan = findCurrentTouchSpan(typeModel, event.x, event.y) + if (touchSpan != null) { + touchSpan.isPressed = true + currentTouchSpan = touchSpan + invalidate() + return true + } + } + } + MotionEvent.ACTION_UP, + MotionEvent.ACTION_CANCEL -> { + val current = currentTouchSpan + if (current != null) { + currentTouchSpan = null + current.isPressed = false + if (event.action == MotionEvent.ACTION_UP) { + current.onClick.invoke(current.start, current.end) + } + invalidate() + return true + } + } + } + return super.onTouchEvent(event) + } + + private fun findCurrentTouchSpan(typeModel: TypeModel, x: Float, y: Float): TouchSpan? { + for (i in 0 until touchSpanList.size) { + val touchSpan = touchSpanList[i] + if (isSpanTouched(typeModel, touchSpan, x, y)) { + return touchSpan + } + } + return null + } + + private fun isSpanTouched(typeModel: TypeModel, touchSpan: TouchSpan, x: Float, y: Float): Boolean { + val start = typeModel.getByPos(touchSpan.start) ?: return false + val end = typeModel.getByPos(touchSpan.end) ?: return false + if (start.y + paddingTop > y || end.y + paddingTop + end.measureHeight < y) { + return false + } else if (start.y == end.y) { // in one line + return !(start.x + paddingLeft > x || end.x + paddingLeft < x) + } else { + // in muti line + if (x < start.x + paddingLeft && y < start.y + start.measureHeight + paddingTop) { + return false + } else if (x > end.x + end.measureWidth + paddingLeft && y > end.y) { + return false + } + return true + } + } + + class TouchSpan(val start: Int, val end: Int, val onClick: (Int, Int) -> Unit) { + var isPressed: Boolean = false + internal set + } +} \ No newline at end of file diff --git a/type/src/main/java/com/qmuiteam/qmui/type/view/MarqueeTypeView.kt b/type/src/main/java/com/qmuiteam/qmui/type/view/MarqueeTypeView.kt new file mode 100644 index 000000000..0381821a6 --- /dev/null +++ b/type/src/main/java/com/qmuiteam/qmui/type/view/MarqueeTypeView.kt @@ -0,0 +1,246 @@ +package com.qmuiteam.qmui.type.view + +import android.content.Context +import android.graphics.* +import android.os.SystemClock +import android.util.AttributeSet +import android.view.accessibility.AccessibilityNodeInfo +import com.qmuiteam.qmui.type.TypeModel +import com.qmuiteam.qmui.type.parser.PlainTextParser +import com.qmuiteam.qmui.type.parser.TextParser + +class MarqueeTypeView : BaseTypeView { + + constructor(context: Context): super(context) + constructor(context: Context, attrs: AttributeSet?): super(context, attrs) + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): super(context, attrs, defStyleAttr) + + var typeModel: TypeModel? = null + var textParser: TextParser = PlainTextParser.instance + set(value) { + if (field != value) { + field = value + reset() + contentWidth = -1 + typeModel = value.parse(text) + requestLayout() + if (isAttachedToWindow) { + start() + } + } + } + + var text: CharSequence? = null + set(value) { + if (field != value) { + field = value + reset() + contentWidth = -1 + typeModel = textParser.parse(value) + requestLayout() + if (isAttachedToWindow) { + start() + } + } + } + + var keepTime: Long = 2000 + var gap: Float = resources.displayMetrics.density * 50 + var moveSpeedPerMs: Float = resources.displayMetrics.density / 36 + var lastDrawTime = -2L + + private var elementMaxHeight = 0 + private var contentWidth = -1 + private var fadeHelper: FadeHelper? = null + private var startX: Float = 0f + + + var fadeWidth: Float + get() = fadeHelper?.fadeWidth ?: 0f + set(value) { + (fadeHelper ?: FadeHelper().also { fadeHelper = it }).fadeWidth = value + invalidate() + } + + override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) { + super.onInitializeAccessibilityNodeInfo(info) + info.text = text + info.contentDescription = text + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val widthSize = MeasureSpec.getSize(widthMeasureSpec) + val widthMode = MeasureSpec.getMode(widthMeasureSpec) + val heightSize = MeasureSpec.getSize(heightMeasureSpec) + val heightMode = MeasureSpec.getMode(heightMeasureSpec) + environment.setMeasureLimit(widthSize, heightSize) + measureAndLayoutModel() + val usedWidth = if (widthMode == MeasureSpec.AT_MOST) { + contentWidth.toInt() + } else widthSize + val usedHeight = if (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED) { + elementMaxHeight.toInt() + } else heightSize + + var current = typeModel?.firstElement() + while (current != null) { + current.y = (usedHeight - current.measureHeight) / 2 + current = current.next + } + setMeasuredDimension(usedWidth, usedHeight) + } + + + private fun measureAndLayoutModel() { + elementMaxHeight = 0 + var current = typeModel?.firstElement() + var x = 0 + while (current != null) { + current.measure(environment) + current.x = x + elementMaxHeight = elementMaxHeight.coerceAtLeast(current.measureHeight) + x += current.measureWidth + current = current.next + } + contentWidth = x + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + if (!text.isNullOrBlank()) { + start() + } + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + stop() + } + + fun start() { + lastDrawTime = -1 + invalidate() + } + + fun stop() { + lastDrawTime = -2 + } + + fun reset() { + startX = 0f + } + + override fun onDraw(canvas: Canvas) { + if(fadeHelper == null){ + drawContent(canvas) + }else{ + fadeHelper!!.drawFade(canvas){ + drawContent(canvas) + } + } + } + + + private fun drawContent(canvas: Canvas){ + if (contentWidth >0 && contentWidth <= width) { + lastDrawTime = -2 + startX = 0f + } + canvas.save() + canvas.translate(startX, 0f) + var current = typeModel?.firstElement() + while (current != null) { + if (current.x + startX > width) { + break + } else if (current.x + startX + current.measureWidth > 0) { + current.draw(environment, canvas) + } + current = current.next + } + canvas.restore() + val gapRight = startX + contentWidth + gap + if (startX < 0 && gapRight < width) { + canvas.save() + canvas.translate(gapRight, 0f) + current = typeModel?.firstElement() + while (current != null) { + if (current.x + gapRight > width) { + break + } else { + current.draw(environment, canvas) + } + current = current.next + } + canvas.restore() + } + if (lastDrawTime == -2L) { + return + } + if (lastDrawTime == -1L) { + lastDrawTime = SystemClock.elapsedRealtime() + if(startX == 0f) keepTime else 0 + } else { + val newTime = SystemClock.elapsedRealtime() + if(lastDrawTime < newTime){ + startX -= moveSpeedPerMs * (newTime - lastDrawTime) + if(startX >= 0f && startX < resources.displayMetrics.density * 1.5){ + // allow 1.5dp error + startX = 0f + lastDrawTime = newTime + keepTime + }else{ + lastDrawTime = newTime + } + + if (startX + contentWidth < 0) { + startX += contentWidth + gap + } + // if drop many many frames. this condition can be matched, so recover to normal state. + if (startX + contentWidth < 0) { + startX = 0f + } + } // else is keep time + } + postInvalidateOnAnimation() + } + + private inner class FadeHelper{ + + var fadeWidth = 0f + + private val paint = Paint().apply { + style = Paint.Style.FILL + xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN) + } + + private val leftFadeShader by lazy(LazyThreadSafetyMode.NONE) { + LinearGradient( + 0f, 0f, fadeWidth, 0f, + intArrayOf(Color.TRANSPARENT, Color.BLACK), null, Shader.TileMode.CLAMP + ) + } + + private val rightFadeShader by lazy(LazyThreadSafetyMode.NONE) { + LinearGradient( + 0f, 0f, fadeWidth, 0f, + intArrayOf(Color.BLACK, Color.TRANSPARENT), null, Shader.TileMode.CLAMP + ) + } + + inline fun drawFade(canvas: Canvas, action: (Canvas) -> Unit) = canvas.apply { + if(fadeWidth <= 0 || contentWidth <= width){ + action(this) + }else{ + val layerId = saveLayer(0f, 0f, width.toFloat(), height.toFloat(), null) + action(this) + if(startX < 0){ + paint.shader = leftFadeShader + drawRect(0f, 0f, fadeWidth, height.toFloat(), paint) + } + + translate((width - fadeWidth).coerceAtLeast(0f), 0f) + paint.shader = rightFadeShader + drawRect(0f, 0f, fadeWidth, height.toFloat(), paint) + restoreToCount(layerId) + } + } + } +} \ No newline at end of file diff --git a/update_from_1_to_2.md b/update_from_1_to_2.md deleted file mode 100644 index e77201408..000000000 --- a/update_from_1_to_2.md +++ /dev/null @@ -1,42 +0,0 @@ -# QMUI 2.x 重要变更 - -## 概述 - -1. 升级到 Androidx -2. minSdkVersion 升级到 API 19 -3. 提供换肤(Dark Mode)支持 -4. `QMUI.Compat` 主题改名为 `QMUI`,并移除旧的 `QMUI` 主题 - -## 组件变更 - -### QMUITopBar - -- 使用 `QMUIQQFaceView` 来承载标题和副标题 -- 更改父类为 `QMUIRelativeLayout`,使用 QMUILayout 的分割线实现,移除配置项:`qmui_topbar_need_separator`,`qmui_topbar_separator_height`, `qmui_topbar_separator_color`。 -- 移除配置项 `qmui_topbar_bg_color`, 使用官方实现`android:background` 替代。 - -### QMUIQQFaceView - -- 可以通过 `QMUIQQFaceCompiler.setDefaultQQFaceManager()` 设置 QQFaceManager, App 不再需要定义一个子类。 - -### QMUITabSegment - -- 完全重构,与旧版本完全不兼容 - -### QMUIPopup - -- 完全重构,与旧版本完全不兼容 - -### QMUIEmptyView - -- 父类更改为 ConstraintLayout -- theme 中 添加间距、颜色、字号 - -### QMUIGroupListView - -- 配置项改名: - - qmui_commonList_titleColor -> qmui_common_list_title_color - - qmui_commonList_detailColor -> qmui_common_list_detail_color -- QMUICommonListItemView 父类更换为 ConstraintLayout, 布局重写 -- 移除 separatorStyle 的配置,使用 QMUILayout 相关的设置 -