├── .gitignore ├── .gitlab-ci.yml ├── LICENSE ├── README.md ├── assets ├── gifs │ └── survey-kit-demo.gif ├── top │ ├── color-picker.png │ ├── how_old_are_you_with_hint.png │ ├── instruction_qbs.png │ ├── known_allergies_with_2_selected.png │ ├── single_choice_selection_1_selection.png │ └── surveykit-logo.png └── unused │ ├── instruction_green.png │ ├── instruction_orange.png │ ├── instruction_start_cyan.png │ ├── instruction_start_german_cyan.png │ ├── integer_question_disabled.png │ ├── multiple_choice_question_1_selected.png │ ├── multiple_choice_question_multiple_selected.png │ ├── multiple_choice_question_no_selected.png │ ├── pick_a_value.png │ ├── scale_question.png │ ├── screenshot_main.png │ ├── text_question_cyan_enabled.png │ └── text_question_disabled_cyan.png ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts └── src │ └── main │ └── kotlin │ ├── ApiKeys.kt │ ├── Deps.kt │ ├── Library.kt │ └── Project.kt ├── example ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── quickbirdstudios │ │ └── example │ │ ├── App.kt │ │ ├── YandexAddressSuggestionProvider.kt │ │ └── ui │ │ └── main │ │ └── MainActivity.kt │ └── res │ ├── drawable │ ├── color1.xml │ ├── color2.xml │ ├── color3.xml │ ├── color4.xml │ ├── color5.xml │ ├── color6.xml │ ├── example2.png │ └── example3.jpg │ ├── layout │ └── main_activity.xml │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── git-hook-install.gradle.kts ├── git-hooks ├── commit-msg ├── install.sh └── pre-push ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── ktlint ├── settings.gradle ├── survey ├── build.gradle.kts └── src │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── fancy_checkmark.json │ ├── java │ │ ├── com.quickbirdstudios.surveykit │ │ │ ├── AnswerFormat.kt │ │ │ ├── FinishReason.kt │ │ │ ├── Identifier.kt │ │ │ ├── NavigationRule.kt │ │ │ ├── SurveyTheme.kt │ │ │ ├── Task.kt │ │ │ ├── backend │ │ │ │ ├── helpers │ │ │ │ │ ├── extensions │ │ │ │ │ │ ├── Context.kt │ │ │ │ │ │ ├── Dimensions.kt │ │ │ │ │ │ ├── Drawable.kt │ │ │ │ │ │ ├── EditText+afterTextChanged.kt │ │ │ │ │ │ └── View.kt │ │ │ │ │ └── logTag.kt │ │ │ │ ├── navigator │ │ │ │ │ ├── NavigableOrderedTaskNavigator.kt │ │ │ │ │ ├── OrderedTaskNavigator.kt │ │ │ │ │ └── TaskNavigator.kt │ │ │ │ ├── presenter │ │ │ │ │ ├── NextAction.kt │ │ │ │ │ ├── Presenter.kt │ │ │ │ │ ├── PresenterImpl.kt │ │ │ │ │ └── animations │ │ │ │ │ │ └── ViewAnimator.kt │ │ │ │ ├── result_gatherer │ │ │ │ │ ├── ResultGatherer.kt │ │ │ │ │ └── ResultGathererImpl.kt │ │ │ │ └── views │ │ │ │ │ ├── main_parts │ │ │ │ │ ├── AbortDialogConfiguration.kt │ │ │ │ │ ├── Content.kt │ │ │ │ │ ├── DialogConfiguration.kt │ │ │ │ │ ├── Dialogs.kt │ │ │ │ │ ├── Footer.kt │ │ │ │ │ ├── Header.kt │ │ │ │ │ └── StyleablePart.kt │ │ │ │ │ ├── question_parts │ │ │ │ │ ├── DatePickerPart.kt │ │ │ │ │ ├── DateTimePickerPart.kt │ │ │ │ │ ├── InfoTextPart.kt │ │ │ │ │ ├── IntegerTextFieldPart.kt │ │ │ │ │ ├── LocationPickerPart.kt │ │ │ │ │ ├── MultipleChoicePart.kt │ │ │ │ │ ├── QuestionAnimation.kt │ │ │ │ │ ├── ScalePart.kt │ │ │ │ │ ├── SingleChoicePart.kt │ │ │ │ │ ├── TextFieldPart.kt │ │ │ │ │ ├── TimePickerPart.kt │ │ │ │ │ ├── ValuePickerPart.kt │ │ │ │ │ ├── helper │ │ │ │ │ │ └── Checkable+createSelectableThemedBackground.kt │ │ │ │ │ └── imageSelector │ │ │ │ │ │ ├── ImageSelectorAdapter.kt │ │ │ │ │ │ └── ImageSelectorPart.kt │ │ │ │ │ ├── questions │ │ │ │ │ ├── BooleanQuestionView.kt │ │ │ │ │ ├── DatePickerQuestionView.kt │ │ │ │ │ ├── DateTimePickerQuestionView.kt │ │ │ │ │ ├── EmailQuestionView.kt │ │ │ │ │ ├── FinishQuestionView.kt │ │ │ │ │ ├── ImageSelectorQuestionView.kt │ │ │ │ │ ├── IntegerQuestionView.kt │ │ │ │ │ ├── IntroQuestionView.kt │ │ │ │ │ ├── LocationPickerQuestionView.kt │ │ │ │ │ ├── MultipleChoiceQuestionView.kt │ │ │ │ │ ├── ScaleQuestionView.kt │ │ │ │ │ ├── SingleChoiceQuestionView.kt │ │ │ │ │ ├── TextQuestionView.kt │ │ │ │ │ ├── TimePickerQuestionView.kt │ │ │ │ │ └── ValuePickerQuestionView.kt │ │ │ │ │ └── step │ │ │ │ │ ├── Identifiable.kt │ │ │ │ │ ├── QuestionView.kt │ │ │ │ │ ├── Skipable.kt │ │ │ │ │ ├── StepView.kt │ │ │ │ │ └── ViewActions.kt │ │ │ ├── result │ │ │ │ ├── QuestionResult.kt │ │ │ │ ├── Result.kt │ │ │ │ ├── StepResult.kt │ │ │ │ ├── TaskResult.kt │ │ │ │ └── question_results │ │ │ │ │ ├── BooleanQuestionResult.kt │ │ │ │ │ ├── DateQuestionResult.kt │ │ │ │ │ ├── DateTimeQuestionResult.kt │ │ │ │ │ ├── EmailQuestionResult.kt │ │ │ │ │ ├── FinishQuestionResult.kt │ │ │ │ │ ├── ImageSelectorResult.kt │ │ │ │ │ ├── IntegerQuestionResult.kt │ │ │ │ │ ├── IntroQuestionResult.kt │ │ │ │ │ ├── LocationQuestionResult.kt │ │ │ │ │ ├── MultipleChoiceQuestionResult.kt │ │ │ │ │ ├── ScaleQuestionResult.kt │ │ │ │ │ ├── SingleChoiceQuestionResult.kt │ │ │ │ │ ├── TextQuestionResult.kt │ │ │ │ │ ├── TimeQuestionResult.kt │ │ │ │ │ └── ValuePickerQuestionResult.kt │ │ │ ├── steps │ │ │ │ ├── CompletionStep.kt │ │ │ │ ├── InstructionStep.kt │ │ │ │ ├── QuestionStep.kt │ │ │ │ └── Step.kt │ │ │ └── survey │ │ │ │ ├── Survey.kt │ │ │ │ └── SurveyView.kt │ │ └── com │ │ │ └── quickbirdstudios │ │ │ └── surveykit │ │ │ ├── backend │ │ │ └── address │ │ │ │ ├── AddressSuggestionProvider.kt │ │ │ │ └── GeocoderAddressSuggestionProvider.kt │ │ │ └── extensions │ │ │ └── ViewExtensions.kt │ └── res │ │ ├── anim │ │ ├── enter_from_left.xml │ │ ├── enter_from_right.xml │ │ ├── exit_to_left.xml │ │ ├── exit_to_right.xml │ │ ├── slide_in_down.xml │ │ ├── slide_in_up.xml │ │ ├── slide_out_down.xml │ │ └── slide_out_up.xml │ │ ├── drawable │ │ ├── check.xml │ │ ├── header_background.xml │ │ ├── ic_baseline_location_on.xml │ │ ├── ic_baseline_search_24.xml │ │ ├── input_border.xml │ │ ├── input_border_bottom_border.xml │ │ ├── input_border_bottom_border_pressed.xml │ │ ├── input_border_pressed.xml │ │ ├── left_arrow.xml │ │ ├── main_button_background.xml │ │ ├── skip_button_background.xml │ │ └── tickmark.xml │ │ ├── layout │ │ ├── custom_step.xml │ │ ├── layout_content.xml │ │ ├── layout_footer.xml │ │ ├── layout_header.xml │ │ ├── layout_image_selector_image.xml │ │ ├── layout_info_field.xml │ │ ├── layout_question.xml │ │ ├── lottie_checkmark_wrapper.xml │ │ ├── main_activity.xml │ │ ├── survey_activity_empty.xml │ │ ├── test_activity.xml │ │ └── view_question.xml │ │ ├── raw │ │ └── fancy_checkmark.json │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── ids.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── quickbirdstudios │ └── test │ ├── NavigableOrderedTaskNavigatorTest.kt │ ├── OrderedTaskNavigatorTest.kt │ ├── ResultGathererTest.kt │ └── TaskNavigatorTest.kt └── test ├── build.gradle.kts └── src ├── androidTest └── java │ └── com │ └── quickbirdstudios │ └── test │ ├── FullSurveyTest.kt │ └── pages │ ├── PageTest+testBooleanChoiceStep.kt │ ├── PageTest+testCompletionStep.kt │ ├── PageTest+testCustomStep.kt │ ├── PageTest+testDatePickerStep.kt │ ├── PageTest+testEmailStep.kt │ ├── PageTest+testImageSelectorStep.kt │ ├── PageTest+testIntroStep.kt │ ├── PageTest+testLocationPickerStep.kt │ ├── PageTest+testMultipleChoiceStep.kt │ ├── PageTest+testNumberStep.kt │ ├── PageTest+testScaleStep.kt │ ├── PageTest+testSingleChoiceStep.kt │ ├── PageTest+testTextStep.kt │ ├── PageTest+testTimePickerStep.kt │ ├── PageTest+testValuePickerStep.kt │ └── PageTest.kt └── main ├── AndroidManifest.xml ├── java └── com │ └── quickbirdstudios │ └── test │ ├── TestActivity.kt │ ├── TestAddressProvider.kt │ └── TestApp.kt └── res ├── drawable ├── example2.png └── example3.jpg └── values └── strings.xml /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | variables: 2 | GIT_CLEAN_FLAGS: none 3 | 4 | stages: 5 | - test 6 | - deploy 7 | 8 | unitTests: 9 | stage: test 10 | tags: 11 | - android 12 | script: 13 | - ./gradlew clean 14 | - ./gradlew testDebugUnitTest 15 | 16 | appTests: 17 | tags: 18 | - android 19 | - emulator 20 | stage: test 21 | script: 22 | - ./gradlew clean 23 | - ./gradlew cDAT 24 | 25 | deployRelease: 26 | stage: deploy 27 | only: 28 | - tags 29 | script: 30 | - ./gradlew publish 31 | 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 QuickBird Studios 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/gifs/survey-kit-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBirdEng/SurveyKit/83cd95cc79db67fb96e642e84b815a4c8b5fb5ab/assets/gifs/survey-kit-demo.gif -------------------------------------------------------------------------------- /assets/top/color-picker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBirdEng/SurveyKit/83cd95cc79db67fb96e642e84b815a4c8b5fb5ab/assets/top/color-picker.png -------------------------------------------------------------------------------- /assets/top/how_old_are_you_with_hint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBirdEng/SurveyKit/83cd95cc79db67fb96e642e84b815a4c8b5fb5ab/assets/top/how_old_are_you_with_hint.png -------------------------------------------------------------------------------- /assets/top/instruction_qbs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBirdEng/SurveyKit/83cd95cc79db67fb96e642e84b815a4c8b5fb5ab/assets/top/instruction_qbs.png -------------------------------------------------------------------------------- /assets/top/known_allergies_with_2_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBirdEng/SurveyKit/83cd95cc79db67fb96e642e84b815a4c8b5fb5ab/assets/top/known_allergies_with_2_selected.png -------------------------------------------------------------------------------- /assets/top/single_choice_selection_1_selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBirdEng/SurveyKit/83cd95cc79db67fb96e642e84b815a4c8b5fb5ab/assets/top/single_choice_selection_1_selection.png -------------------------------------------------------------------------------- /assets/top/surveykit-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBirdEng/SurveyKit/83cd95cc79db67fb96e642e84b815a4c8b5fb5ab/assets/top/surveykit-logo.png -------------------------------------------------------------------------------- /assets/unused/instruction_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBirdEng/SurveyKit/83cd95cc79db67fb96e642e84b815a4c8b5fb5ab/assets/unused/instruction_green.png -------------------------------------------------------------------------------- /assets/unused/instruction_orange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBirdEng/SurveyKit/83cd95cc79db67fb96e642e84b815a4c8b5fb5ab/assets/unused/instruction_orange.png -------------------------------------------------------------------------------- /assets/unused/instruction_start_cyan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBirdEng/SurveyKit/83cd95cc79db67fb96e642e84b815a4c8b5fb5ab/assets/unused/instruction_start_cyan.png -------------------------------------------------------------------------------- /assets/unused/instruction_start_german_cyan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBirdEng/SurveyKit/83cd95cc79db67fb96e642e84b815a4c8b5fb5ab/assets/unused/instruction_start_german_cyan.png -------------------------------------------------------------------------------- /assets/unused/integer_question_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBirdEng/SurveyKit/83cd95cc79db67fb96e642e84b815a4c8b5fb5ab/assets/unused/integer_question_disabled.png -------------------------------------------------------------------------------- /assets/unused/multiple_choice_question_1_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBirdEng/SurveyKit/83cd95cc79db67fb96e642e84b815a4c8b5fb5ab/assets/unused/multiple_choice_question_1_selected.png -------------------------------------------------------------------------------- /assets/unused/multiple_choice_question_multiple_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBirdEng/SurveyKit/83cd95cc79db67fb96e642e84b815a4c8b5fb5ab/assets/unused/multiple_choice_question_multiple_selected.png -------------------------------------------------------------------------------- /assets/unused/multiple_choice_question_no_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBirdEng/SurveyKit/83cd95cc79db67fb96e642e84b815a4c8b5fb5ab/assets/unused/multiple_choice_question_no_selected.png -------------------------------------------------------------------------------- /assets/unused/pick_a_value.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBirdEng/SurveyKit/83cd95cc79db67fb96e642e84b815a4c8b5fb5ab/assets/unused/pick_a_value.png -------------------------------------------------------------------------------- /assets/unused/scale_question.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBirdEng/SurveyKit/83cd95cc79db67fb96e642e84b815a4c8b5fb5ab/assets/unused/scale_question.png -------------------------------------------------------------------------------- /assets/unused/screenshot_main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBirdEng/SurveyKit/83cd95cc79db67fb96e642e84b815a4c8b5fb5ab/assets/unused/screenshot_main.png -------------------------------------------------------------------------------- /assets/unused/text_question_cyan_enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBirdEng/SurveyKit/83cd95cc79db67fb96e642e84b815a4c8b5fb5ab/assets/unused/text_question_cyan_enabled.png -------------------------------------------------------------------------------- /assets/unused/text_question_disabled_cyan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBirdEng/SurveyKit/83cd95cc79db67fb96e642e84b815a4c8b5fb5ab/assets/unused/text_question_disabled_cyan.png -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompile 2 | 3 | buildscript { 4 | repositories { 5 | mavenCentral() 6 | google() 7 | maven { url = uri("https://repository.quickbirdstudios.com/repository/public") } 8 | } 9 | 10 | dependencies { 11 | classpath(Deps.Plugins.kotlin) 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | mavenCentral() 18 | google() 19 | } 20 | 21 | tasks.withType(KotlinJvmCompile::class.java).all { 22 | kotlinOptions { 23 | jvmTarget = "1.8" 24 | freeCompilerArgs = listOf( 25 | "-Xuse-experimental=kotlin.Experimental", 26 | "-Xuse-experimental=kotlinx.coroutines.ObsoleteCoroutinesApi", 27 | "-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi", 28 | "-Xuse-experimental=kotlin.ExperimentalStdlibApi" 29 | ) 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | 2 | 3 | plugins { 4 | `kotlin-dsl` 5 | } 6 | 7 | repositories { 8 | mavenCentral() 9 | google() 10 | } 11 | 12 | dependencies { 13 | implementation(gradleApi()) 14 | implementation(localGroovy()) 15 | implementation("com.android.tools.build:gradle:4.0.2") 16 | } 17 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/ApiKeys.kt: -------------------------------------------------------------------------------- 1 | package com.quickbirdstudios.surveykit 2 | 3 | import org.gradle.api.Project 4 | import java.io.IOException 5 | import java.util.* 6 | 7 | object ApiKeys { 8 | 9 | fun Project.yandexMapsKey(): String { 10 | return propertyOrEmpty("yandex_sdk_key") 11 | } 12 | 13 | fun Project.googleMapsKey(): String { 14 | return propertyOrEmpty("google_sdk_key") 15 | } 16 | 17 | private fun Project.propertyOrEmpty(name: String): String { 18 | return findPropertyCasted(name) 19 | .fallback { findInLocalPropertiesFile(name) } 20 | .fallback { findByProperties(name) } ?: "" 21 | } 22 | 23 | private fun Project.findPropertyCasted(name: String): String? = findProperty(name) as String? 24 | 25 | private fun Project.findInLocalPropertiesFile(name: String): String? { 26 | val properties = Properties() 27 | try { 28 | properties.load(project.rootProject.file("local.properties").inputStream()) 29 | } catch (e: IOException) { 30 | println("Error trying to load local.properties file") 31 | } 32 | return properties[name] as? String? 33 | } 34 | 35 | private fun Project.findByProperties(name: String): String? { 36 | return properties[name] as String? 37 | } 38 | 39 | private fun String?.fallback(block: () -> String?): String? { 40 | return this ?: block() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/Deps.kt: -------------------------------------------------------------------------------- 1 | object Versions { 2 | 3 | const val kotlin = "1.3.50" 4 | const val coroutines = "1.3.0" 5 | const val test = "1.1.1" 6 | const val jUnit = "5.5.2" 7 | const val jUnitPlatform = "1.5.2" 8 | const val lottie = "3.0.7" 9 | const val espresso = "3.1.0" 10 | 11 | object AndroidSupport { 12 | const val appCompat = "1.0.0" 13 | const val constraintLayout = "1.1.3" 14 | const val annotation = "1.0.0" 15 | const val recyclerView = "1.0.0" 16 | } 17 | 18 | object PlayServices { 19 | const val maps = "17.0.0" 20 | } 21 | 22 | object Google { 23 | const val material = "1.2.1" 24 | } 25 | } 26 | 27 | object Deps { 28 | 29 | object Plugins { 30 | const val kotlin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}" 31 | } 32 | 33 | object Kotlin { 34 | const val stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${Versions.kotlin}" 35 | const val reflect = "org.jetbrains.kotlin:kotlin-reflect:${Versions.kotlin}" 36 | const val coroutines = 37 | "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutines}" 38 | const val androidCoroutines = 39 | "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.coroutines}" 40 | } 41 | 42 | object PlayServices { 43 | const val maps = "com.google.android.gms:play-services-maps:${Versions.PlayServices.maps}" 44 | } 45 | 46 | object Google { 47 | const val material = "com.google.android.material:material:${Versions.Google.material}" 48 | } 49 | 50 | object AndroidSupport { 51 | const val appCompat = "androidx.appcompat:appcompat:${Versions.AndroidSupport.appCompat}" 52 | const val constraint = 53 | "androidx.constraintlayout:constraintlayout:${Versions.AndroidSupport.constraintLayout}" 54 | const val recyclerView = 55 | "androidx.recyclerview:recyclerview:${Versions.AndroidSupport.recyclerView}" 56 | } 57 | 58 | object Test { 59 | const val jUnitJupiter = "org.junit.jupiter:junit-jupiter-engine:${Versions.jUnit}" 60 | const val jUnitPlatform = 61 | "org.junit.platform:junit-platform-runner:${Versions.jUnitPlatform}" 62 | const val core = "androidx.test:core:${Versions.test}" 63 | const val runner = "androidx.test:runner:${Versions.test}" 64 | const val rules = "androidx.test:rules:${Versions.test}" 65 | const val junitExt = "androidx.test.ext:junit:${Versions.test}" 66 | const val espresso = "androidx.test.espresso:espresso-core:${Versions.espresso}" 67 | const val espressoIntents = "androidx.test.espresso:espresso-intents:${Versions.espresso}" 68 | const val espressoContribs = "androidx.test.espresso:espresso-contrib:${Versions.espresso}" 69 | } 70 | 71 | const val lottie = "com.airbnb.android:lottie:${Versions.lottie}" 72 | } 73 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/Library.kt: -------------------------------------------------------------------------------- 1 | object Library { 2 | const val version = "2.0.0-alpha-4" 3 | } 4 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/Project.kt: -------------------------------------------------------------------------------- 1 | object Project { 2 | object Android { 3 | const val compileSdkVersion = 28 4 | const val targetSdkVersion = 28 5 | const val minSdkVersion = 21 6 | const val testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /example/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.quickbirdstudios.surveykit.ApiKeys.googleMapsKey 2 | import com.quickbirdstudios.surveykit.ApiKeys.yandexMapsKey 3 | 4 | plugins { 5 | id("com.android.application") 6 | kotlin("android") 7 | id("org.jetbrains.kotlin.android.extensions") 8 | } 9 | 10 | androidExtensions { isExperimental = true } 11 | 12 | android { 13 | compileSdkVersion(Project.Android.compileSdkVersion) 14 | 15 | defaultConfig { 16 | versionName = Library.version 17 | versionCode = 1 18 | minSdkVersion(Project.Android.minSdkVersion) 19 | targetSdkVersion(Project.Android.targetSdkVersion) 20 | testInstrumentationRunner = Project.Android.testInstrumentationRunner 21 | resValue("string", "google_api_key", googleMapsKey()) 22 | resValue("string", "yandex_api_key", yandexMapsKey()) 23 | } 24 | packagingOptions { 25 | exclude("META-INF/*kotlin*") 26 | } 27 | } 28 | 29 | dependencies { 30 | implementation(project(":survey")) 31 | 32 | /* Kotlin */ 33 | implementation(Deps.Kotlin.stdlib) 34 | implementation(Deps.Kotlin.reflect) 35 | implementation(Deps.Kotlin.coroutines) 36 | 37 | /* Android Support */ 38 | implementation(Deps.AndroidSupport.appCompat) 39 | implementation(Deps.AndroidSupport.constraint) 40 | } 41 | -------------------------------------------------------------------------------- /example/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 23 | 24 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /example/src/main/java/com/quickbirdstudios/example/App.kt: -------------------------------------------------------------------------------- 1 | package com.quickbirdstudios.example 2 | 3 | import android.app.Application 4 | 5 | class App : Application() 6 | -------------------------------------------------------------------------------- /example/src/main/res/drawable/color1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /example/src/main/res/drawable/color2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /example/src/main/res/drawable/color3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /example/src/main/res/drawable/color4.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /example/src/main/res/drawable/color5.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /example/src/main/res/drawable/color6.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /example/src/main/res/drawable/example2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBirdEng/SurveyKit/83cd95cc79db67fb96e642e84b815a4c8b5fb5ab/example/src/main/res/drawable/example2.png -------------------------------------------------------------------------------- /example/src/main/res/drawable/example3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBirdEng/SurveyKit/83cd95cc79db67fb96e642e84b815a4c8b5fb5ab/example/src/main/res/drawable/example3.jpg -------------------------------------------------------------------------------- /example/src/main/res/layout/main_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /example/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #008577 4 | #00574B 5 | #36BFCC 6 | 7 | #33C5D4 8 | #3CD0DF 9 | #33B8C5 10 | 11 | #48E924 12 | #40D31F 13 | #38B41C 14 | 15 | #E75B30 16 | #DF552B 17 | #AA4020 18 | 19 | #000000 20 | 21 | -------------------------------------------------------------------------------- /example/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Welcome to the\nQuickBird Studios\nHealth Survey 6 | Get ready for a bunch of super random questions! 7 | Let\'s go! 8 | 9 | Tell us about you 10 | Tell us about yourself and why you want to improve your health. 11 | 12 | How old are you? 13 | 14 | 15 | Select your body type 16 | 17 | 1 18 | 5 19 | 20 | Known allergies 21 | Do you have any allergies that we should be aware of? 22 | Penicillin 23 | Latex 24 | Pet 25 | Pollen 26 | 27 | Done? 28 | We are done, do you mind to tell us more about yourself? 29 | 30 | Done! 31 | Thanks for taking the survey, we will contact you soon! 32 | Submit survey 33 | 34 | Survey-Kit Test App 35 | Please enter your age 36 | Pick a value 37 | Here you can pick any value you\'d like! 38 | BooleanAnswerFormat example title 39 | BooleanAnswerFormat example text 40 | Date Picker title 41 | Date Picker text 42 | Time Picker title 43 | Time Picker text 44 | Email question text 45 | Email question title 46 | Image choice 47 | Pick a color 48 | DateTime Question 49 | Choose a date and time 50 | Are you sure you want to quit the survey? 51 | SurveyKit 52 | Yes 53 | No 54 | Select your home address 55 | Tell us about yourself and your home address. 56 | 57 | 58 | -------------------------------------------------------------------------------- /example/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 18 | 19 | -------------------------------------------------------------------------------- /git-hook-install.gradle.kts: -------------------------------------------------------------------------------- 1 | import java.io.File 2 | 3 | /* 4 | ################################################################################################ 5 | GLOBALS 6 | ################################################################################################ 7 | */ 8 | val hookStartIdentifier = "#QBS-HOOK-START" 9 | val hookEndIdentifier = "#QBS-HOOK-END" 10 | val existingGitHookFolder = File(".git${File.separator}hooks") 11 | val existingGitHooks by lazy { 12 | existingGitHookFolder 13 | .listFiles() 14 | ?.filterNot { it.isSample() } 15 | ?: throw IllegalStateException("Missing ${existingGitHookFolder.absolutePath}") 16 | } 17 | val projectSpecificHookFolder = File("git-hooks") 18 | val projectSpecificGitHooks = projectSpecificHookFolder.listFiles() 19 | /* 20 | ################################################################################################ 21 | HELPER FUNCTIONS 22 | ################################################################################################ 23 | */ 24 | fun copyGitHook(folder: File, gitHook: File) { 25 | val target = File(folder, gitHook.name) 26 | val wrappedHook = gitHook.readText().wrapInHookIdentifiers() 27 | target.writeText(wrappedHook) 28 | target.setExecutable(true) 29 | } 30 | 31 | fun getExistingGitHook(fileName: String) = 32 | existingGitHooks.firstOrNull { file -> file.name == fileName } 33 | 34 | fun File.isSample() = name.endsWith(".sample") 35 | fun File.insertHookIfNeeded(text: String) { 36 | val fileContent = readText() 37 | /* 38 | hook is up to date 39 | */ 40 | if (fileContent.contains(text)) return 41 | val hook = text.wrapInHookIdentifiers() 42 | /* 43 | hook already exists, update it 44 | */ 45 | if (fileContent.containsQbHook()) { 46 | val startIndex = fileContent.indexOf(hookStartIdentifier) 47 | val endIndex = fileContent.indexOf(hookEndIdentifier) + hookEndIdentifier.length 48 | val updatedContent = fileContent.replaceRange(startIndex, endIndex, hook) 49 | this.writeText(updatedContent) 50 | } else { 51 | /* 52 | hook isn't installed, append to the current file 53 | */ 54 | this.appendText("\n\n$hook") 55 | } 56 | } 57 | 58 | fun String.containsQbHook() = contains(hookStartIdentifier) && contains(hookEndIdentifier) 59 | fun String.wrapInHookIdentifiers() = "$hookStartIdentifier\n$this\n$hookEndIdentifier" 60 | /* 61 | ################################################################################################ 62 | TASK 63 | ################################################################################################ 64 | */ 65 | task("installGitHooks") { 66 | try { 67 | projectSpecificGitHooks.forEach { gitHook -> 68 | 69 | val existingHook = getExistingGitHook(gitHook.name) 70 | if (existingHook != null) { 71 | existingHook.insertHookIfNeeded(gitHook.readText()) 72 | } else { 73 | copyGitHook(existingGitHookFolder, gitHook) 74 | } 75 | } 76 | } catch (e: Throwable) { 77 | print("Something went wrong while installing the git hooks: $e.message") 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /git-hooks/commit-msg: -------------------------------------------------------------------------------- 1 | #QBS-HOOK-START 2 | #!/usr/bin/env bash 3 | # Automatically adds branch name every commit message. 4 | NAME=$(git branch | grep '*' | sed 's/* //') 5 | MESSAGE="$(cat "$1")" 6 | FIRST_MESSAGE_CHAR=${MESSAGE:0:1} 7 | 8 | if [[ ${MESSAGE} != *${NAME}* ]] 9 | then 10 | if [[ ${FIRST_MESSAGE_CHAR} == "-" ]] 11 | then 12 | echo "$NAME:\n $(cat "$1")" > "$1" 13 | else 14 | echo "$NAME: $(cat "$1")" > "$1" 15 | fi 16 | fi 17 | #QBS-HOOK-END 18 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536m 2 | kotlin.code.style=official 3 | org.gradle.parallel=true 4 | android.useAndroidX=true 5 | android.enableJetifier=true 6 | repositoryName=SurveyKit 7 | projectSummary=Create beautiful surveys on Android -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBirdEng/SurveyKit/83cd95cc79db67fb96e642e84b815a4c8b5fb5ab/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Apr 01 15:38:24 CEST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /ktlint: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBirdEng/SurveyKit/83cd95cc79db67fb96e642e84b815a4c8b5fb5ab/ktlint -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | google() 5 | maven { url 'https://repository.quickbirdstudios.com/repository/public' } 6 | } 7 | 8 | dependencies { 9 | classpath("com.quickbirdstudios:gradle-publishing:0.1.0") 10 | } 11 | } 12 | 13 | rootProject.name = 'SurveyKit' 14 | 15 | include ':survey' 16 | include ':test' 17 | include ':example' 18 | -------------------------------------------------------------------------------- /survey/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("SuspiciousCollectionReassignment") 2 | 3 | plugins { 4 | id("com.android.library") 5 | kotlin("android") 6 | id("org.jetbrains.kotlin.android.extensions") 7 | id("gradle-publishing") 8 | } 9 | 10 | androidExtensions { isExperimental = true } 11 | 12 | android { 13 | compileSdkVersion(Project.Android.compileSdkVersion) 14 | 15 | defaultConfig { 16 | version = Library.version 17 | minSdkVersion(Project.Android.minSdkVersion) 18 | targetSdkVersion(Project.Android.targetSdkVersion) 19 | testInstrumentationRunner = Project.Android.testInstrumentationRunner 20 | } 21 | 22 | testOptions { 23 | animationsDisabled = true 24 | } 25 | } 26 | 27 | dependencies { 28 | implementation(Deps.Kotlin.stdlib) 29 | api(Deps.Kotlin.coroutines) 30 | implementation(Deps.Kotlin.androidCoroutines) 31 | implementation(Deps.AndroidSupport.appCompat) 32 | implementation(Deps.AndroidSupport.constraint) 33 | implementation(Deps.AndroidSupport.recyclerView) 34 | implementation(Deps.lottie) 35 | implementation(Deps.PlayServices.maps) 36 | implementation(Deps.Google.material) 37 | 38 | testImplementation(Deps.Test.jUnitJupiter) 39 | testImplementation(Deps.Test.jUnitPlatform) 40 | } -------------------------------------------------------------------------------- /survey/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /survey/src/main/java/com.quickbirdstudios.surveykit/FinishReason.kt: -------------------------------------------------------------------------------- 1 | package com.quickbirdstudios.surveykit 2 | 3 | // TODO check if Saved can be removed 4 | enum class FinishReason { 5 | Saved, Discarded, Completed, Failed; 6 | } 7 | -------------------------------------------------------------------------------- /survey/src/main/java/com.quickbirdstudios.surveykit/Identifier.kt: -------------------------------------------------------------------------------- 1 | package com.quickbirdstudios.surveykit 2 | 3 | import android.os.Parcelable 4 | import java.util.UUID 5 | import kotlinx.android.parcel.Parcelize 6 | 7 | @Parcelize 8 | open class Identifier(open val id: String) : Parcelable 9 | 10 | @Parcelize 11 | data class TaskIdentifier(override val id: String = UUID.randomUUID().toString()) : 12 | Identifier(id), 13 | Parcelable 14 | 15 | @Parcelize 16 | data class StepIdentifier( 17 | override val id: String = UUID.randomUUID().toString() 18 | ) : Identifier(id), Parcelable 19 | -------------------------------------------------------------------------------- /survey/src/main/java/com.quickbirdstudios.surveykit/NavigationRule.kt: -------------------------------------------------------------------------------- 1 | package com.quickbirdstudios.surveykit 2 | 3 | sealed class NavigationRule { 4 | data class DirectStepNavigationRule( 5 | val destinationStepStepIdentifier: StepIdentifier 6 | ) : NavigationRule() 7 | 8 | data class ConditionalDirectionStepNavigationRule( 9 | val resultToStepIdentifierMapper: (String) -> StepIdentifier? 10 | ) : NavigationRule() 11 | } 12 | -------------------------------------------------------------------------------- /survey/src/main/java/com.quickbirdstudios.surveykit/SurveyTheme.kt: -------------------------------------------------------------------------------- 1 | package com.quickbirdstudios.surveykit 2 | 3 | import androidx.annotation.ColorInt 4 | import com.quickbirdstudios.surveykit.backend.views.main_parts.AbortDialogConfiguration 5 | 6 | // Todo rename to configuration for next major version 7 | data class SurveyTheme( 8 | @ColorInt val themeColorDark: Int, 9 | @ColorInt val themeColor: Int, 10 | @ColorInt val textColor: Int, 11 | val abortDialogConfiguration: AbortDialogConfiguration? = null 12 | ) 13 | -------------------------------------------------------------------------------- /survey/src/main/java/com.quickbirdstudios.surveykit/Task.kt: -------------------------------------------------------------------------------- 1 | package com.quickbirdstudios.surveykit 2 | 3 | import com.quickbirdstudios.surveykit.steps.Step 4 | 5 | interface Task { 6 | val id: TaskIdentifier 7 | val steps: List 8 | 9 | operator fun get(id: StepIdentifier) = steps.find { it.id == id } 10 | } 11 | 12 | open class OrderedTask( 13 | override val steps: List, 14 | override val id: TaskIdentifier = TaskIdentifier() 15 | ) : Task 16 | 17 | class NavigableOrderedTask( 18 | override val steps: List, 19 | override val id: TaskIdentifier = TaskIdentifier() 20 | ) : Task { 21 | 22 | private val stepIdentifierToNavigationRuleMap: MutableMap = 23 | mutableMapOf() 24 | 25 | fun setNavigationRule( 26 | forTriggerStepStepIdentifier: StepIdentifier, 27 | navigationRule: NavigationRule 28 | ) { 29 | stepIdentifierToNavigationRuleMap[forTriggerStepStepIdentifier] = navigationRule 30 | } 31 | 32 | internal fun getRule(id: StepIdentifier): NavigationRule? = 33 | stepIdentifierToNavigationRuleMap[id] 34 | } 35 | -------------------------------------------------------------------------------- /survey/src/main/java/com.quickbirdstudios.surveykit/backend/helpers/extensions/Context.kt: -------------------------------------------------------------------------------- 1 | package com.quickbirdstudios.surveykit.backend.helpers.extensions 2 | 3 | import android.Manifest 4 | import android.content.Context 5 | import android.content.pm.PackageManager 6 | import androidx.core.app.ActivityCompat 7 | 8 | fun Context.isLocationPermissionGranted(): Boolean { 9 | return ( 10 | ActivityCompat.checkSelfPermission( 11 | this, 12 | Manifest.permission.ACCESS_FINE_LOCATION 13 | ) == PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission( 14 | this, 15 | Manifest.permission.ACCESS_COARSE_LOCATION 16 | ) == PackageManager.PERMISSION_GRANTED 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /survey/src/main/java/com.quickbirdstudios.surveykit/backend/helpers/extensions/Dimensions.kt: -------------------------------------------------------------------------------- 1 | package com.quickbirdstudios.surveykit.backend.helpers.extensions 2 | 3 | import android.content.Context 4 | import android.util.DisplayMetrics 5 | 6 | fun Context.dp(px: Number): Float { 7 | val resources = this.resources 8 | val metrics = resources.displayMetrics 9 | return ( 10 | px.toFloat() / 11 | (metrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT.toFloat()) 12 | ) 13 | } 14 | 15 | fun Context.px(dp: Number): Float { 16 | val resources = this.resources 17 | val metrics = resources.displayMetrics 18 | return ( 19 | dp.toFloat() * 20 | (metrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT.toFloat()) 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /survey/src/main/java/com.quickbirdstudios.surveykit/backend/helpers/extensions/Drawable.kt: -------------------------------------------------------------------------------- 1 | package com.quickbirdstudios.surveykit.backend.helpers.extensions 2 | 3 | import android.graphics.drawable.Drawable 4 | import android.graphics.drawable.GradientDrawable 5 | 6 | fun GradientDrawable.colorStroke(width: Int, color: Int): Drawable { 7 | val drawable = this.mutate() as GradientDrawable 8 | drawable.setStroke(width, color) 9 | return drawable 10 | } 11 | -------------------------------------------------------------------------------- /survey/src/main/java/com.quickbirdstudios.surveykit/backend/helpers/extensions/EditText+afterTextChanged.kt: -------------------------------------------------------------------------------- 1 | package com.quickbirdstudios.surveykit.backend.helpers.extensions 2 | 3 | import android.text.Editable 4 | import android.text.TextWatcher 5 | import android.widget.EditText 6 | 7 | fun EditText.afterTextChanged(listener: (String) -> Unit) { 8 | addTextChangedListener(object : TextWatcher { 9 | override fun afterTextChanged(s: Editable) = listener(s.toString()) 10 | override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit 11 | override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /survey/src/main/java/com.quickbirdstudios.surveykit/backend/helpers/extensions/View.kt: -------------------------------------------------------------------------------- 1 | package com.quickbirdstudios.surveykit.backend.helpers.extensions 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import android.view.inputmethod.InputMethodManager 6 | import android.widget.LinearLayout 7 | 8 | fun View.verticalMargin(margin: Int) { 9 | (this.layoutParams as LinearLayout.LayoutParams).setMargins(0, margin, 0, margin) 10 | } 11 | 12 | fun View.horizontalMargin(margin: Int) { 13 | (this.layoutParams as LinearLayout.LayoutParams).setMargins(margin, 0, margin, 0) 14 | } 15 | 16 | fun View.hideKeyboard() { 17 | val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager 18 | imm.hideSoftInputFromWindow(windowToken, 0) 19 | } 20 | 21 | -------------------------------------------------------------------------------- /survey/src/main/java/com.quickbirdstudios.surveykit/backend/helpers/logTag.kt: -------------------------------------------------------------------------------- 1 | package com.quickbirdstudios.surveykit.backend.helpers 2 | 3 | val Any.logTag: String get() = this::class.java.simpleName.orEmpty().take(23) 4 | -------------------------------------------------------------------------------- /survey/src/main/java/com.quickbirdstudios.surveykit/backend/navigator/NavigableOrderedTaskNavigator.kt: -------------------------------------------------------------------------------- 1 | package com.quickbirdstudios.surveykit.backend.navigator 2 | 3 | import com.quickbirdstudios.surveykit.NavigableOrderedTask 4 | import com.quickbirdstudios.surveykit.NavigationRule 5 | import com.quickbirdstudios.surveykit.result.StepResult 6 | import com.quickbirdstudios.surveykit.steps.Step 7 | import java.util.Stack 8 | 9 | internal class NavigableOrderedTaskNavigator( 10 | override val task: NavigableOrderedTask 11 | ) : TaskNavigator { 12 | 13 | //region Members 14 | 15 | override var history: Stack = Stack() 16 | 17 | //endregion 18 | 19 | //region Public API 20 | 21 | override fun startStep(stepResult: StepResult?): Step? { 22 | val previousStep = peekHistory() 23 | return if (previousStep == null) { 24 | task.steps.firstOrNull() 25 | } else { 26 | nextStep(previousStep, stepResult) 27 | } 28 | } 29 | 30 | override fun finalStep(): Step? = task.steps.lastOrNull() 31 | 32 | override fun nextStep(step: Step, stepResult: StepResult?): Step? { 33 | step.record() 34 | 35 | return when (val rule = step.extractRule() ?: return step.nextInList()) { 36 | is NavigationRule.DirectStepNavigationRule -> rule.evaluateNextStep() 37 | is NavigationRule.ConditionalDirectionStepNavigationRule -> 38 | rule.evaluateNextStep(step, stepResult) 39 | } 40 | } 41 | 42 | override fun previousStep(step: Step): Step? { 43 | return if (history.empty()) null 44 | else history.pop() 45 | } 46 | 47 | //endregion 48 | 49 | //region Private Helper 50 | 51 | private fun Step.extractRule(): NavigationRule? = task.getRule(this.id) 52 | 53 | private fun NavigationRule.DirectStepNavigationRule.evaluateNextStep() = 54 | task[this.destinationStepStepIdentifier] 55 | 56 | private fun NavigationRule.ConditionalDirectionStepNavigationRule.evaluateNextStep( 57 | step: Step, 58 | stepResult: StepResult? 59 | ): Step? { 60 | stepResult ?: return step.nextInList() 61 | val firstResult = stepResult.results.firstOrNull() ?: return step.nextInList() 62 | val nextStepIdentifier = this.resultToStepIdentifierMapper(firstResult.stringIdentifier) 63 | ?: return step.nextInList() 64 | 65 | return task[nextStepIdentifier] 66 | } 67 | 68 | //endregion 69 | } 70 | -------------------------------------------------------------------------------- /survey/src/main/java/com.quickbirdstudios.surveykit/backend/navigator/OrderedTaskNavigator.kt: -------------------------------------------------------------------------------- 1 | package com.quickbirdstudios.surveykit.backend.navigator 2 | 3 | import com.quickbirdstudios.surveykit.OrderedTask 4 | import com.quickbirdstudios.surveykit.result.StepResult 5 | import com.quickbirdstudios.surveykit.steps.Step 6 | import java.util.Stack 7 | 8 | internal class OrderedTaskNavigator( 9 | override val task: OrderedTask 10 | ) : TaskNavigator { 11 | 12 | //region Public API 13 | 14 | override var history: Stack = Stack() 15 | 16 | override fun startStep(stepResult: StepResult?): Step? { 17 | val previousStep = peekHistory() 18 | return if (previousStep == null) task.steps.firstOrNull() 19 | else previousStep.nextInList() 20 | } 21 | 22 | override fun finalStep(): Step? = task.steps.lastOrNull() 23 | 24 | override fun nextStep(step: Step, stepResult: StepResult?): Step? { 25 | step.record() 26 | return step.nextInList() 27 | } 28 | 29 | override fun previousStep(step: Step): Step? = step.previousInList() 30 | 31 | //endregion 32 | 33 | //region Private Helper 34 | 35 | private fun Step.previousInList(): Step? { 36 | val currentStepIndex = task.steps.indexOf(this) 37 | return if (currentStepIndex - 1 < 0) null 38 | else task.steps[currentStepIndex - 1] 39 | } 40 | 41 | //endregion 42 | } 43 | -------------------------------------------------------------------------------- /survey/src/main/java/com.quickbirdstudios.surveykit/backend/navigator/TaskNavigator.kt: -------------------------------------------------------------------------------- 1 | package com.quickbirdstudios.surveykit.backend.navigator 2 | 3 | import com.quickbirdstudios.surveykit.NavigableOrderedTask 4 | import com.quickbirdstudios.surveykit.OrderedTask 5 | import com.quickbirdstudios.surveykit.Task 6 | import com.quickbirdstudios.surveykit.result.StepResult 7 | import com.quickbirdstudios.surveykit.steps.Step 8 | import java.util.Stack 9 | 10 | interface TaskNavigator { 11 | 12 | val task: Task 13 | 14 | var history: Stack 15 | 16 | fun lastStepInHistory(): Step? = peekHistory() 17 | 18 | fun startStep(stepResult: StepResult?): Step? 19 | 20 | fun finalStep(): Step? 21 | 22 | fun nextStep(step: Step, stepResult: StepResult? = null): Step? 23 | 24 | fun previousStep(step: Step): Step? 25 | 26 | fun Step.nextInList(): Step? { 27 | val currentStepIndex = task.steps.indexOf(this) 28 | return if (currentStepIndex + 1 > task.steps.size - 1) null 29 | else task.steps[currentStepIndex + 1] 30 | } 31 | 32 | fun peekHistory(): Step? { 33 | if (history.isEmpty()) return null 34 | return history.peek() 35 | } 36 | 37 | fun hasPreviousStep() : Boolean { 38 | val previousStep = peekHistory() 39 | return previousStep != null 40 | } 41 | 42 | fun Step?.record() { 43 | if (this != null) { 44 | history.push(this) 45 | } 46 | } 47 | 48 | companion object { 49 | operator fun invoke(task: Task): TaskNavigator = 50 | when (task) { 51 | is OrderedTask -> OrderedTaskNavigator( 52 | task 53 | ) 54 | is NavigableOrderedTask -> NavigableOrderedTaskNavigator( 55 | task 56 | ) 57 | else -> throw NotImplementedError() 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /survey/src/main/java/com.quickbirdstudios.surveykit/backend/presenter/NextAction.kt: -------------------------------------------------------------------------------- 1 | package com.quickbirdstudios.surveykit.backend.presenter 2 | 3 | import com.quickbirdstudios.surveykit.FinishReason 4 | import com.quickbirdstudios.surveykit.result.StepResult 5 | 6 | sealed class NextAction { 7 | data class Next(val result: StepResult) : NextAction() 8 | data class Back(val result: StepResult) : NextAction() 9 | object Skip : NextAction() 10 | data class Close(val result: StepResult, val finishReason: FinishReason) : NextAction() 11 | } 12 | -------------------------------------------------------------------------------- /survey/src/main/java/com.quickbirdstudios.surveykit/backend/presenter/Presenter.kt: -------------------------------------------------------------------------------- 1 | package com.quickbirdstudios.surveykit.backend.presenter 2 | 3 | import android.content.Context 4 | import android.widget.FrameLayout 5 | import com.quickbirdstudios.surveykit.SurveyTheme 6 | import com.quickbirdstudios.surveykit.backend.navigator.TaskNavigator 7 | import com.quickbirdstudios.surveykit.result.StepResult 8 | import com.quickbirdstudios.surveykit.steps.Step 9 | 10 | interface Presenter { 11 | val context: Context 12 | val viewContainer: FrameLayout 13 | val surveyTheme: SurveyTheme 14 | val taskNavigator: TaskNavigator 15 | 16 | suspend operator fun invoke( 17 | transition: Transition, 18 | step: Step, 19 | stepResult: StepResult? 20 | ): NextAction 21 | fun triggerBackOnCurrentView() 22 | 23 | enum class Transition { 24 | None, SlideFromRight, SlideFromLeft; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /survey/src/main/java/com.quickbirdstudios.surveykit/backend/result_gatherer/ResultGatherer.kt: -------------------------------------------------------------------------------- 1 | package com.quickbirdstudios.surveykit.backend.result_gatherer 2 | 3 | import com.quickbirdstudios.surveykit.StepIdentifier 4 | import com.quickbirdstudios.surveykit.result.StepResult 5 | import com.quickbirdstudios.surveykit.result.TaskResult 6 | 7 | interface ResultGatherer { 8 | val taskResult: TaskResult 9 | var results: MutableList 10 | 11 | fun store(stepResult: StepResult) 12 | fun retrieve(identifier: StepIdentifier): StepResult? 13 | } 14 | -------------------------------------------------------------------------------- /survey/src/main/java/com.quickbirdstudios.surveykit/backend/result_gatherer/ResultGathererImpl.kt: -------------------------------------------------------------------------------- 1 | package com.quickbirdstudios.surveykit.backend.result_gatherer 2 | 3 | import com.quickbirdstudios.surveykit.StepIdentifier 4 | import com.quickbirdstudios.surveykit.Task 5 | import com.quickbirdstudios.surveykit.result.StepResult 6 | import com.quickbirdstudios.surveykit.result.TaskResult 7 | import java.util.Date 8 | 9 | internal class ResultGathererImpl(private val task: Task) : ResultGatherer { 10 | 11 | private val startDate: Date = Date() 12 | 13 | override var results: MutableList = mutableListOf() 14 | 15 | override val taskResult: TaskResult 16 | get() = TaskResult( 17 | id = task.id, 18 | results = results, 19 | startDate = startDate 20 | ).apply { endDate = Date() } 21 | 22 | override fun store(stepResult: StepResult) { 23 | val previousResult = retrieve(stepResult.id) 24 | if (previousResult == null) results.add(stepResult) 25 | else { 26 | val previousResultIndex = results.indexOf(previousResult) 27 | results[previousResultIndex] = stepResult 28 | } 29 | } 30 | 31 | override fun retrieve(identifier: StepIdentifier) = results.find { it.id == identifier } 32 | } 33 | -------------------------------------------------------------------------------- /survey/src/main/java/com.quickbirdstudios.surveykit/backend/views/main_parts/AbortDialogConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.quickbirdstudios.surveykit.backend.views.main_parts 2 | 3 | import androidx.annotation.StringRes 4 | 5 | data class AbortDialogConfiguration( 6 | @StringRes val title: Int, 7 | @StringRes val message: Int, 8 | @StringRes val neutralMessage: Int, 9 | @StringRes val negativeMessage: Int 10 | ) 11 | -------------------------------------------------------------------------------- /survey/src/main/java/com.quickbirdstudios.surveykit/backend/views/main_parts/Content.kt: -------------------------------------------------------------------------------- 1 | package com.quickbirdstudios.surveykit.backend.views.main_parts 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.LinearLayout 8 | import androidx.core.widget.NestedScrollView 9 | import com.quickbirdstudios.surveykit.R 10 | import com.quickbirdstudios.surveykit.SurveyTheme 11 | 12 | class Content @JvmOverloads constructor( 13 | context: Context, 14 | attrs: AttributeSet? = null, 15 | defStyleRes: Int = 0 16 | ) : NestedScrollView(context, attrs, defStyleRes), StyleablePart { 17 | 18 | //region Member 19 | 20 | private val root: View = View.inflate(context, R.layout.layout_content, this) 21 | private val container: ViewGroup = root.findViewById(R.id.content_container) 22 | private val footerContainer: ViewGroup = root.findViewById(R.id.footer_container) 23 | private val footer: Footer = Footer(context).apply { id = R.id.questionFooter } 24 | 25 | //endregion 26 | 27 | //region Public API 28 | 29 | fun add(view: T): T { 30 | container.addView(view) 31 | return view 32 | } 33 | 34 | fun clear() { 35 | container.removeAllViews() 36 | } 37 | 38 | //endregion 39 | 40 | //region Overrides 41 | 42 | override fun style(surveyTheme: SurveyTheme) { 43 | (0 until container.childCount).forEach { 44 | (container.getChildAt(it) as StyleablePart).style(surveyTheme) 45 | } 46 | footer.style(surveyTheme) 47 | } 48 | 49 | //endregion 50 | 51 | init { 52 | this.layoutParams = LinearLayout.LayoutParams( 53 | LinearLayout.LayoutParams.MATCH_PARENT, 54 | 0 55 | ).apply { 56 | this.weight = 1f 57 | } 58 | footerContainer.addView(footer) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /survey/src/main/java/com.quickbirdstudios.surveykit/backend/views/main_parts/DialogConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.quickbirdstudios.surveykit.backend.views.main_parts 2 | 3 | data class DialogConfiguration( 4 | val title: String, 5 | val message: String, 6 | val neutralMessage: String, 7 | val negativeMessage: String 8 | ) 9 | -------------------------------------------------------------------------------- /survey/src/main/java/com.quickbirdstudios.surveykit/backend/views/main_parts/Dialogs.kt: -------------------------------------------------------------------------------- 1 | package com.quickbirdstudios.surveykit.backend.views.main_parts 2 | 3 | import android.app.AlertDialog 4 | import android.content.Context 5 | import android.content.DialogInterface 6 | import android.graphics.Color 7 | 8 | internal class Dialogs { 9 | 10 | companion object { 11 | fun cancel( 12 | context: Context, 13 | abortDialogConfiguration: AbortDialogConfiguration, 14 | cancelAction: () -> Unit 15 | ): AlertDialog? { 16 | val dialog = AlertDialog.Builder(context) 17 | .setMessage(abortDialogConfiguration.message) 18 | .setCancelable(true) 19 | .setNegativeButton(abortDialogConfiguration.negativeMessage) { _, which -> 20 | if (which == DialogInterface.BUTTON_NEGATIVE) 21 | cancelAction() 22 | } 23 | .setNeutralButton(abortDialogConfiguration.neutralMessage) { _, _ -> } 24 | .create() 25 | 26 | dialog.setTitle(abortDialogConfiguration.title) 27 | dialog.show() 28 | dialog.setButtonColors() 29 | return dialog 30 | } 31 | } 32 | } 33 | 34 | private fun AlertDialog.setButtonColors() { 35 | val negativeButton = this.getButton(DialogInterface.BUTTON_NEGATIVE) ?: return 36 | negativeButton.setTextColor(Color.RED) 37 | } 38 | -------------------------------------------------------------------------------- /survey/src/main/java/com.quickbirdstudios.surveykit/backend/views/main_parts/Header.kt: -------------------------------------------------------------------------------- 1 | package com.quickbirdstudios.surveykit.backend.views.main_parts 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.graphics.Color 6 | import android.util.AttributeSet 7 | import android.view.View 8 | import android.view.inputmethod.InputMethodManager 9 | import android.widget.Button 10 | import android.widget.ImageView 11 | import android.widget.RelativeLayout 12 | import android.widget.TextView 13 | import androidx.appcompat.widget.Toolbar 14 | import com.quickbirdstudios.surveykit.R 15 | import com.quickbirdstudios.surveykit.SurveyTheme 16 | 17 | // TODO should take [Configuration] in constructor and remove public color setters and getters 18 | class Header @JvmOverloads constructor( 19 | context: Context, 20 | attrs: AttributeSet? = null, 21 | defStyleRes: Int = 0 22 | ) : Toolbar(context, attrs, defStyleRes), 23 | StyleablePart { 24 | 25 | //region Public API 26 | 27 | var themeColor = Color.RED 28 | set(value) { 29 | cancelButton.setTextColor(value) 30 | headerBackButtonImage.background.setTint(value) 31 | field = value 32 | } 33 | 34 | var canCancel: Boolean 35 | get() = cancelButton.visibility != View.VISIBLE 36 | set(value) { 37 | cancelButton.visibility = if (value) View.VISIBLE else View.GONE 38 | } 39 | 40 | var canBack: Boolean 41 | get() = buttonBack.visibility != View.VISIBLE 42 | set(value) { 43 | buttonBack.visibility = if (value) View.VISIBLE else View.GONE 44 | } 45 | 46 | var cancelButtonText: String 47 | get() = cancelButton.text.toString() 48 | set(value) { 49 | cancelButton.text = value 50 | } 51 | 52 | var label: String 53 | get() = headerLabel.text.toString() 54 | set(label) { 55 | headerLabel.text = label 56 | } 57 | 58 | var onBack: () -> Unit = {} 59 | var onCancel: () -> Unit = {} 60 | 61 | //endregion 62 | 63 | //region Members 64 | 65 | private val root: View = View.inflate(context, R.layout.layout_header, this) 66 | private val headerLabel: TextView = root.findViewById(R.id.headerLabel) 67 | private val buttonBack = root.findViewById(R.id.headerBackButton).apply { 68 | setOnClickListener { 69 | hideKeyboard() 70 | onBack() 71 | } 72 | } 73 | private val headerBackButtonImage = 74 | buttonBack.findViewById(R.id.headerBackButtonImage).apply { 75 | background.setTint(themeColor) 76 | } 77 | private val cancelButton = root.findViewById