├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build.sbt ├── project ├── .DS_Store ├── Libraries.scala ├── ReplacePropertiesGenerator.scala ├── Settings.scala ├── Versions.scala ├── build.properties └── plugins.sbt ├── scripts └── runPackage.sh └── src ├── .DS_Store ├── main ├── .DS_Store ├── AndroidManifest.xml ├── assets │ └── activities.json ├── res │ ├── drawable-hdpi │ │ ├── ic_add.png │ │ └── icon_app.png │ ├── drawable-mdpi │ │ └── icon_app.png │ ├── drawable-v21 │ │ ├── background_default_fab.xml │ │ └── background_default_list.xml │ ├── drawable-xhdpi │ │ ├── ic_add.png │ │ └── icon_app.png │ ├── drawable-xxhdpi │ │ ├── forecast_01.png │ │ ├── forecast_02.png │ │ ├── forecast_03.png │ │ ├── forecast_04.png │ │ ├── forecast_09.png │ │ ├── forecast_10.png │ │ ├── forecast_11.png │ │ ├── forecast_13.png │ │ ├── forecast_50.png │ │ ├── ic_add.png │ │ ├── icon_app.png │ │ ├── map_marker.png │ │ └── unknown.png │ ├── drawable-xxxhdpi │ │ └── icon_app.png │ ├── drawable │ │ ├── background_default_fab.xml │ │ ├── background_default_fab_circle.xml │ │ ├── background_default_fab_shadow.xml │ │ ├── background_default_list.xml │ │ ├── background_error_button.xml │ │ ├── background_item_api_advised.xml │ │ ├── background_item_api_required.xml │ │ ├── background_item_api_success.xml │ │ ├── background_item_circle.xml │ │ ├── background_item_circle_activated.xml │ │ ├── background_item_circle_pressed.xml │ │ ├── background_item_icon_selector.xml │ │ ├── background_item_pathmorphsample_1.xml │ │ ├── background_item_pathmorphsample_2.xml │ │ ├── background_item_pathmorphsample_3.xml │ │ ├── background_item_pathmorphsample_4.xml │ │ ├── background_item_square.xml │ │ └── placeholder_circle.xml │ ├── layout │ │ ├── image_item.xml │ │ └── material_list_activity.xml │ ├── menu │ │ ├── activity_forecast.xml │ │ └── activity_google_maps.xml │ ├── values-v21 │ │ ├── colors.xml │ │ └── themes.xml │ └── values │ │ ├── app_config.xml │ │ ├── colors.xml │ │ ├── config.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ ├── styles.xml │ │ └── themes.xml ├── resources │ └── application.conf └── scala │ ├── .DS_Store │ └── com │ ├── .DS_Store │ └── fortysevendeg │ ├── .DS_Store │ └── scala │ ├── .DS_Store │ └── android │ ├── .DS_Store │ ├── modules │ ├── forecast │ │ ├── Messages.scala │ │ ├── impl │ │ │ └── ForecastServices.scala │ │ └── model │ │ │ ├── ApiModel.scala │ │ │ ├── Conversions.scala │ │ │ └── Forecast.scala │ └── utils │ │ └── NetUtils.scala │ └── ui │ ├── akkasimon │ ├── AkkaSimonActivity.scala │ ├── Layout.scala │ ├── Styles.scala │ ├── actors │ │ ├── ColorActor.scala │ │ └── ComputerActor.scala │ ├── fragments │ │ ├── ColorFragment.scala │ │ └── ComputerFragment.scala │ └── util │ │ ├── FragmentEnum.scala │ │ └── SimonAkkaFragment.scala │ ├── apirequest │ ├── ForecastApiRequestActivity.scala │ ├── ForecastFragment.scala │ ├── Layout.scala │ └── Styles.scala │ ├── circularreveal │ ├── CircularRevealActivity.scala │ ├── Layout.scala │ ├── SampleFragment.scala │ └── Styles.scala │ ├── commons │ ├── CommonsStyles.scala │ ├── JsonReads.scala │ ├── ScalaExtraTweaks.scala │ └── ToolbarLayout.scala │ ├── components │ ├── CircleView.scala │ ├── CircularTransformation.scala │ ├── PathMorphDrawable.scala │ └── RippleBackgroundView.scala │ ├── googlemaps │ ├── CustomMapFragment.scala │ ├── GoogleMapsActivity.scala │ ├── Layout.scala │ ├── Styles.scala │ └── googlemaps.scala │ ├── main │ ├── Layout.scala │ ├── MainActivity.scala │ ├── MainItemDecorator.scala │ ├── ProjectActivityInfoListAdapter.scala │ └── Styles.scala │ ├── materiallist │ ├── Composers.scala │ ├── FABAnimationBehavior.scala │ ├── ImageListAdapter.scala │ └── MaterialListActivity.scala │ ├── pathmorphing │ ├── Layout.scala │ ├── PathMorphingActivity.scala │ └── Styles.scala │ ├── ripplebackground │ ├── Layout.scala │ ├── RippleBackgroundActivity.scala │ └── Styles.scala │ └── textstyles │ ├── Layout.scala │ ├── Styles.scala │ └── TextStylesActivity.scala └── test ├── resources └── weather.json └── scala └── com └── fortysevendeg └── scala └── android ├── BaseTestSpecification.scala ├── ContextWrapperContextTestSupport.scala └── modules └── forecast ├── ForecastServicesSpec.scala └── model └── JsonModelSpec.scala /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | logs 3 | target 4 | tmp 5 | .history 6 | dist 7 | /out 8 | /RUNNING_PID 9 | /.ivy* 10 | 11 | # sbt specific 12 | /.sbt 13 | .cache/ 14 | .history/ 15 | .lib/ 16 | dist/* 17 | target/ 18 | lib_managed/ 19 | src_managed/ 20 | project/boot/ 21 | project/plugins/project/ 22 | project/project 23 | project/target 24 | /.activator 25 | 26 | # Scala-IDE specific 27 | .scala_dependencies 28 | .worksheet 29 | 30 | #Eclipse specific 31 | .classpath 32 | .project 33 | .cache 34 | .settings/ 35 | 36 | #IntelliJ IDEA specific 37 | .idea/ 38 | /.idea_modules 39 | /.idea 40 | /*.iml 41 | 42 | #Proguard 43 | proguard-sbt.txt 44 | 45 | #Properties 46 | local.properties 47 | debug.properties 48 | release.properties 49 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | scala: 3 | - 2.11.7 4 | before_install: 5 | - sudo apt-get update -qq 6 | - sudo apt-get install libc6-i386 lib32z1 lib32stdc++6 7 | - wget http://dl.google.com/android/android-sdk_r24-linux.tgz 8 | - tar xf android-sdk_r24-linux.tgz 9 | - export ANDROID_HOME=$PWD/android-sdk-linux 10 | - export ANDROID_SDK_HOME=$PWD/android-sdk-linux 11 | - export PATH=${PATH}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools 12 | - echo yes | android update sdk --all --filter platform-tools --no-ui 13 | - echo yes | android update sdk --all --filter build-tools-23.0.1 --no-ui 14 | - echo yes | android update sdk --all --filter android-23 --no-ui 15 | - echo yes | android update sdk --all --filter extra-android-support --no-ui 16 | - echo yes | android update sdk --all --filter extra-android-m2repository --no-ui 17 | - echo yes | android update sdk --all --filter extra-google-m2repository --no-ui 18 | - echo yes | android update sdk --all --filter extra-google-google_play_services --no-ui 19 | script: 20 | - sbt ++$TRAVIS_SCALA_VERSION test 21 | - ./scripts/runPackage.sh 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/47deg/scala-android.svg?branch=master)](https://travis-ci.org/47deg/scala-android) 2 | 3 | Scala API Demos 4 | ============= 5 | 6 | This repository contains examples of using Scala on Android. The [Macroid](http://macroid.github.io/) library is used in the project to assist in GUI operations. 7 | 8 | You can download the project from [Google Play](https://play.google.com/store/apps/details?id=com.fortysevendeg.scala.android) 9 | 10 | Compile 11 | ====== 12 | 13 | You can compile this project and contribute improvements. To compile the project: 14 | 15 | * Download [Activator](https://www.lightbend.com/community/core-tools/activator-and-sbt) and install it 16 | * Configure the Android SDK on your computer 17 | * Clone this GitHub project to your computer 18 | * From project root directory run: 19 | 20 | ``` 21 | $ activator 22 | ``` 23 | or 24 | 25 | ``` 26 | $ sbt 27 | ``` 28 | 29 | * Connect your phone and execute: 30 | 31 | ``` 32 | > run 33 | ``` 34 | 35 | You can use your favorite IDE. At 47 Degrees we use IntelliJ with the Scala plugin. If you want to run this project from IntelliJ you only need to import the project. 36 | 37 | Add Debug Keys 38 | ======== 39 | 40 | You need to add a `debug.properties` file to the root project with the necessary keys to compile. The content should be: 41 | 42 | ``` 43 | openweather.api.key=*** 44 | google.map.key=*** 45 | ``` 46 | 47 | Contribute your own examples 48 | =============== 49 | 50 | If you want to learn *Scala on Android* and you want to share your examples, you can send us a PR with your new feature. 51 | 52 | Follow these steps to create your example: 53 | 54 | * Create a new package inside `ui` for your sample 55 | * The package for a feature contains all of the UI information (it's not necessary but you should consider it). This information is: 56 | * Activities 57 | * Fragments 58 | * Adapters 59 | * Styles: this file replaces the XML Resources. All styles are defined in this file 60 | * Layouts: this file replaces the XML Resources. All layouts are defined in this file 61 | * Add your activity to `AndroidManifest.xml` 62 | * Create a new node in `activities.json` inside the `asset` directory. The JSON should look like this: 63 | 64 | ``` 65 | { 66 | "name": "Name of your example", 67 | "description": "Description of your example", 68 | "className": "Path of your activity", 69 | "minApi": "An integer designating the minimum API Level required ", 70 | "targetApi": "An integer designating the API Level that the application targets", 71 | "scalaLevel": "An integer designating the Scala Level of your example [1,2,3]", 72 | "androidLevel": "An integer designating the Android Level of your example [1,2,3]", 73 | "user" : { 74 | "avatar": "Your avatar URL", 75 | "name": "Your name", 76 | "twitter": "Your twitter username" 77 | } 78 | } 79 | ``` 80 | 81 | ``` 82 | 1 -> Beginner 83 | 2 -> Intermediate 84 | 3 -> Advanced 85 | ``` 86 | 87 | If you are having trouble deciding on an example to contribute here are some ideas: 88 | 89 | * Transitions between activities: use the new [Activity Transitions in Material Design](https://developer.android.com/training/material/animations.html#Transitions), similar to *Google Play Music* 90 | * Validate forms with [Validation in ScalaZ](http://eed3si9n.com/learning-scalaz/Validation.html) 91 | 92 | 93 | License 94 | ====== 95 | 96 | Copyright (C) 2015 47 Degrees, LLC http://47deg.com hello@47deg.com 97 | 98 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 99 | 100 | http://www.apache.org/licenses/LICENSE-2.0 101 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 102 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import Libraries.android._ 2 | import Libraries.macroid._ 3 | import Libraries.akka._ 4 | import Libraries.playServices._ 5 | import Libraries.graphics._ 6 | import Libraries.json._ 7 | import Libraries.net._ 8 | import Libraries.test._ 9 | import ReplacePropertiesGenerator._ 10 | import android.PromptPasswordsSigningConfig 11 | 12 | android.Plugin.androidBuild 13 | 14 | platformTarget in Android := Versions.androidPlatformV 15 | 16 | name := "scala-android" 17 | 18 | organization := "com.fortysevendeg" 19 | 20 | organizationName := "47 Degrees" 21 | 22 | organizationHomepage := Some(new URL("http://47deg.com")) 23 | 24 | version := Versions.appV 25 | 26 | scalaVersion := Versions.scalaV 27 | 28 | scalacOptions ++= Seq("-feature", "-deprecation") 29 | 30 | javacOptions ++= Seq("-source", "1.7", "-target", "1.7") 31 | 32 | scalacOptions ++= Seq("-feature", "-deprecation", "-target:jvm-1.7") 33 | 34 | resolvers ++= Settings.resolvers 35 | 36 | libraryDependencies ++= Seq( 37 | aar(macroidRoot), 38 | aar(macroidAkkaFragments), 39 | aar(androidDesign), 40 | aar(androidCardView), 41 | aar(androidRecyclerview), 42 | aar(macroidExtras), 43 | aar(playServicesMaps), 44 | playJson, 45 | picasso, 46 | communicator, 47 | akkaActor, 48 | specs2, 49 | mockito, 50 | androidTest) 51 | 52 | transitiveAndroidLibs in Android := true 53 | 54 | run <<= run in Android 55 | 56 | apkSigningConfig in Android := Option( 57 | PromptPasswordsSigningConfig( 58 | keystore = new File(Path.userHome.absolutePath + "/.android/signed.keystore"), 59 | alias = "47deg")) 60 | 61 | proguardScala in Android := true 62 | 63 | useProguard in Android := true 64 | 65 | useProguardInDebug in Android := true 66 | 67 | proguardCache in Android := Seq.empty 68 | 69 | proguardOptions in Android ++= Settings.proguardCommons ++ Settings.proguardAkka 70 | 71 | packagingOptions in Android := PackagingOptions( 72 | Seq("META-INF/LICENSE", 73 | "META-INF/LICENSE.txt", 74 | "META-INF/NOTICE", 75 | "META-INF/NOTICE.txt")) 76 | 77 | dexMaxHeap in Android := "2048m" 78 | 79 | dexMulti in Android := true 80 | 81 | packageRelease <<= (packageRelease in Android).dependsOn(setDebugTask(false)) 82 | 83 | packageResources in Android <<= (packageResources in Android).dependsOn(replaceValuesTask) 84 | -------------------------------------------------------------------------------- /project/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xebia-functional/scala-android/fa7222921dc79620e7410fab84b7cb4af86a19c2/project/.DS_Store -------------------------------------------------------------------------------- /project/Libraries.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | object Libraries { 4 | 5 | def onCompile(dep: ModuleID): ModuleID = dep % "compile" 6 | def onTest(dep: ModuleID): ModuleID = dep % "test" 7 | 8 | object scala { 9 | 10 | lazy val scalaReflect = "org.scala-lang" % "scala-reflect" % Versions.scalaV 11 | lazy val scalap = "org.scala-lang" % "scalap" % Versions.scalaV 12 | } 13 | 14 | object android { 15 | 16 | def androidDep(module: String) = "com.android.support" % module % Versions.androidV 17 | 18 | lazy val androidRecyclerview = androidDep("recyclerview-v7") 19 | lazy val androidCardView = androidDep("cardview-v7") 20 | lazy val androidDesign = androidDep("design") 21 | } 22 | 23 | object playServices { 24 | 25 | def playServicesDep(module: String) = "com.google.android.gms" % module % Versions.playServicesV 26 | 27 | lazy val playServicesGooglePlus = playServicesDep("play-services-plus") 28 | lazy val playServicesAccountLogin = playServicesDep("play-services-identity") 29 | lazy val playServicesActivityRecognition = playServicesDep("play-services-location") 30 | lazy val playServicesAppIndexing = playServicesDep("play-services-appindexing") 31 | lazy val playServicesCast = playServicesDep("play-services-cast") 32 | lazy val playServicesDrive = playServicesDep("play-services-drive") 33 | lazy val playServicesFit = playServicesDep("play-services-fitness") 34 | lazy val playServicesMaps = playServicesDep("play-services-maps") 35 | lazy val playServicesAds = playServicesDep("play-services-ads") 36 | lazy val playServicesPanoramaViewer = playServicesDep("play-services-panorama") 37 | lazy val playServicesGames = playServicesDep("play-services-games") 38 | lazy val playServicesWallet = playServicesDep("play-services-wallet") 39 | lazy val playServicesWear = playServicesDep("play-services-wearable") 40 | // Google Actions, Google Analytics and Google Cloud Messaging 41 | lazy val playServicesBase = playServicesDep("play-services-base") 42 | } 43 | 44 | object graphics { 45 | lazy val picasso = "com.squareup.picasso" % "picasso" % Versions.picassoV 46 | } 47 | 48 | object akka { 49 | 50 | def akka(module: String) = "com.typesafe.akka" %% s"akka-$module" % Versions.akkaV 51 | 52 | lazy val akkaActor = akka("actor") 53 | lazy val akkaTestKit = akka("testkit") 54 | 55 | } 56 | 57 | object macroid { 58 | 59 | def macroid(module: String = "") = 60 | "org.macroid" %% s"macroid${if(!module.isEmpty) s"-$module" else ""}" % Versions.macroidV 61 | 62 | lazy val macroidRoot = macroid() 63 | lazy val macroidAkkaFragments = macroid("akka") 64 | lazy val macroidExtras = "com.fortysevendeg" %% "macroid-extras" % Versions.macroidExtras 65 | } 66 | 67 | object json { 68 | lazy val playJson = "com.typesafe.play" %% "play-json" % Versions.playJsonV 69 | } 70 | 71 | object net { 72 | lazy val communicator = "io.taig" %% "communicator" % Versions.communicatorV 73 | 74 | } 75 | 76 | object test { 77 | lazy val specs2 = "org.specs2" %% "specs2-core" % Versions.specs2V % "test" 78 | lazy val androidTest = "com.google.android" % "android" % "4.1.1.4" % "test" 79 | lazy val mockito = "org.specs2" % "specs2-mock_2.11" % Versions.mockitoV % "test" 80 | } 81 | } -------------------------------------------------------------------------------- /project/ReplacePropertiesGenerator.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 47 Degrees, LLC http://47deg.com hello@47deg.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | * not use this file except in compliance with the License. You may obtain 6 | * a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import java.io.{File, FileInputStream} 18 | import java.util.Properties 19 | 20 | import android.Keys._ 21 | import sbt._ 22 | 23 | import scala.annotation.tailrec 24 | import scala.collection.JavaConverters._ 25 | 26 | object ReplacePropertiesGenerator { 27 | 28 | val debugPropertiesFile = "debug.properties" 29 | 30 | val releasePropertiesFile = "release.properties" 31 | 32 | var debug = true 33 | 34 | def propertiesMap(): Map[String, String] = { 35 | (loadPropertiesFile map { file => 36 | val properties = new Properties() 37 | properties.load(new FileInputStream(file)) 38 | properties.asScala.toMap 39 | }) getOrElse Map.empty 40 | } 41 | 42 | private def namePropertyInConfig(name: String) = s"$${$name}" 43 | 44 | private def loadPropertiesFile: Option[File] = { 45 | val file = new File(if (debug) debugPropertiesFile else releasePropertiesFile) 46 | if (file.exists()) Some(file) else None 47 | } 48 | 49 | def replaceContent(valuesFile: File) = { 50 | val properties = propertiesMap() 51 | val content = IO.readLines(valuesFile) map (replaceLine(properties, _)) 52 | IO.write(valuesFile, content.mkString("\n")) 53 | } 54 | 55 | private def replaceLine(properties: Map[String, String], line: String) = { 56 | @tailrec 57 | def replace(properties: Map[String, String], line: String): String = { 58 | if (properties.isEmpty) { 59 | line 60 | } else { 61 | val (key, value) = properties.head 62 | val name = namePropertyInConfig(key) 63 | replace(properties.tail, if (line.contains(name)) line.replace(name, value) else line) 64 | } 65 | } 66 | replace(properties, line) 67 | } 68 | 69 | def replaceValuesTask = Def.task[Seq[File]] { 70 | try { 71 | val dir: (File, File) = (collectResources in Android).value 72 | val valuesFile: File = new File(dir._2, "/values/values.xml") 73 | replaceContent(valuesFile) 74 | Seq(valuesFile) 75 | } catch { 76 | case e: Throwable => 77 | println("An error occurred loading values.xml") 78 | throw e 79 | } 80 | } 81 | 82 | def setDebugTask(debug: Boolean) = Def.task[Unit] { 83 | this.debug = debug 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /project/Settings.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | object Settings { 4 | 5 | lazy val resolvers = 6 | Seq( 7 | Resolver.mavenLocal, 8 | DefaultMavenRepository, 9 | "jcenter" at "http://jcenter.bintray.com", 10 | "47 Degrees Bintray Repo" at "http://dl.bintray.com/47deg/maven", 11 | Resolver.typesafeRepo("releases"), 12 | Resolver.typesafeRepo("snapshots"), 13 | Resolver.typesafeIvyRepo("snapshots"), 14 | Resolver.sonatypeRepo("releases"), 15 | Resolver.sonatypeRepo("snapshots"), 16 | Resolver.defaultLocal, 17 | "Scalaz Bintray Repo" at "http://dl.bintray.com/scalaz/releases" 18 | ) 19 | 20 | lazy val proguardCommons = Seq( 21 | "-ignorewarnings", 22 | "-keep class scala.Dynamic", 23 | "-keep class com.fortysevendeg.scala.android.** { *; }", 24 | "-keep class macroid.** { *; }", 25 | "-keep class android.** { *; }") 26 | 27 | lazy val proguardAkka = Seq( 28 | "-keep class akka.actor.Actor$class { *; }", 29 | "-keep class akka.actor.LightArrayRevolverScheduler { *; }", 30 | "-keep class akka.actor.LocalActorRefProvider { *; }", 31 | "-keep class akka.actor.CreatorFunctionConsumer { *; }", 32 | "-keep class akka.actor.TypedCreatorFunctionConsumer { *; }", 33 | "-keep class akka.dispatch.BoundedDequeBasedMessageQueueSemantics { *; }", 34 | "-keep class akka.dispatch.UnboundedMessageQueueSemantics { *; }", 35 | "-keep class akka.dispatch.UnboundedDequeBasedMessageQueueSemantics { *; }", 36 | "-keep class akka.dispatch.DequeBasedMessageQueueSemantics { *; }", 37 | "-keep class akka.dispatch.MultipleConsumerSemantics { *; }", 38 | "-keep class akka.actor.LocalActorRefProvider$Guardian { *; }", 39 | "-keep class akka.actor.LocalActorRefProvider$SystemGuardian { *; }", 40 | "-keep class akka.dispatch.UnboundedMailbox { *; }", 41 | "-keep class akka.actor.DefaultSupervisorStrategy { *; }", 42 | "-keep class macroid.akkafragments.AkkaAndroidLogger { *; }", 43 | "-keep class akka.event.Logging$LogExt { *; }") 44 | } 45 | -------------------------------------------------------------------------------- /project/Versions.scala: -------------------------------------------------------------------------------- 1 | object Versions { 2 | 3 | val appV = "1.0.0" 4 | val scalaV = "2.11.7" 5 | val androidPlatformV = "android-23" 6 | val androidV = "23.0.1" 7 | val macroidExtras = "0.2" 8 | val macroidV = "2.0.0-20150427" 9 | val akkaV = "2.3.6" 10 | val playServicesV = "6.5.87" 11 | val playJsonV = "2.3.4" 12 | val picassoV = "2.5.0" 13 | val specs2V = "3.6.1" 14 | val mockitoV = "3.6.1" 15 | val communicatorV = "2.0.0" 16 | } 17 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.9 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | logLevel := Level.Info 2 | addSbtPlugin("org.scala-android" % "sbt-android" % "1.6.0") 3 | -------------------------------------------------------------------------------- /scripts/runPackage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ev 3 | sbt ++$TRAVIS_SCALA_VERSION clean 4 | sbt ++$TRAVIS_SCALA_VERSION android:package 5 | sbt ++$TRAVIS_SCALA_VERSION android:package 6 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xebia-functional/scala-android/fa7222921dc79620e7410fab84b7cb4af86a19c2/src/.DS_Store -------------------------------------------------------------------------------- /src/main/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xebia-functional/scala-android/fa7222921dc79620e7410fab84b7cb4af86a19c2/src/main/.DS_Store -------------------------------------------------------------------------------- /src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 24 | 25 | 28 | 29 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/main/assets/activities.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Material Design List", 4 | "description": "This example shows how to use a list with the AppBar and FabButton from the Android Support Design Library", 5 | "className": "com.fortysevendeg.scala.android.ui.materiallist.MaterialListActivity", 6 | "minApi": 14, 7 | "targetApi": 21, 8 | "scalaLevel": 1, 9 | "androidLevel": 2, 10 | "user" : { 11 | "avatar": "http://www.47deg.com/assets/img/company/profile-javi.jpg", 12 | "name": "Javi Pacheco", 13 | "twitter": "@javielinux" 14 | } 15 | }, 16 | { 17 | "name": "Ripple Background", 18 | "description": "This example demonstrates usage of the new Ripple animations available in Lollipop and how to create several animations in serial using Macroid", 19 | "className": "com.fortysevendeg.scala.android.ui.ripplebackground.RippleBackgroundActivity", 20 | "minApi": 21, 21 | "targetApi": 21, 22 | "scalaLevel": 1, 23 | "androidLevel": 2, 24 | "user" : { 25 | "avatar": "http://www.47deg.com/assets/img/company/profile-javi.jpg", 26 | "name": "Javi Pacheco", 27 | "twitter": "@javielinux" 28 | } 29 | }, 30 | { 31 | "name": "Path Morphing", 32 | "description": "This example demonstrates a new drawable created for this project called PathMorphDrawable, used for path segment transformations in Canvas", 33 | "className": "com.fortysevendeg.scala.android.ui.pathmorphing.PathMorphingActivity", 34 | "minApi": 14, 35 | "targetApi": 14, 36 | "scalaLevel": 2, 37 | "androidLevel": 2, 38 | "user" : { 39 | "avatar": "http://www.47deg.com/assets/img/company/profile-paco.jpg", 40 | "name": "Francisco Díaz", 41 | "twitter": "@francisco_dr" 42 | } 43 | }, 44 | { 45 | "name": "Circular Reveal", 46 | "description": "This example demonstrates Lollipop's new Circular Reveal and how it can be used in a Fragment. On non Lollipop devices it will fallback to using a fade animation", 47 | "className": "com.fortysevendeg.scala.android.ui.circularreveal.CircularRevealActivity", 48 | "minApi": 14, 49 | "targetApi": 21, 50 | "scalaLevel": 1, 51 | "androidLevel": 1, 52 | "user" : { 53 | "avatar": "http://www.47deg.com/assets/img/company/profile-javi.jpg", 54 | "name": "Javi Pacheco", 55 | "twitter": "@javielinux" 56 | } 57 | }, 58 | { 59 | "name": "Google Maps", 60 | "description": "A simple example of using Google Maps in a project. Includes - changing map type and adding markers", 61 | "className": "com.fortysevendeg.scala.android.ui.googlemaps.GoogleMapsActivity", 62 | "minApi": 14, 63 | "targetApi": 14, 64 | "scalaLevel": 1, 65 | "androidLevel": 2, 66 | "user" : { 67 | "avatar": "http://www.47deg.com/assets/img/company/profile-fede.jpg", 68 | "name": "Fede Fernández", 69 | "twitter": "@fede_fdz" 70 | } 71 | }, 72 | { 73 | "name": "Text Styles", 74 | "description": "A basic example to demonstrate project setup and define layouts and styles using Macroid", 75 | "className": "com.fortysevendeg.scala.android.ui.textstyles.TextStylesActivity", 76 | "minApi": 14, 77 | "targetApi": 14, 78 | "scalaLevel": 1, 79 | "androidLevel": 1, 80 | "user" : { 81 | "avatar": "http://www.47deg.com/assets/img/company/profile-javi.jpg", 82 | "name": "Javi Pacheco", 83 | "twitter": "@javielinux" 84 | } 85 | }, 86 | { 87 | "name": "Akka Simon", 88 | "description": "Using the classic game Simon, this example demonstrates the use of Akka to communicate between fragments", 89 | "className": "com.fortysevendeg.scala.android.ui.akkasimon.AkkaSimonActivity", 90 | "minApi": 14, 91 | "targetApi": 14, 92 | "scalaLevel": 3, 93 | "androidLevel": 2, 94 | "user" : { 95 | "avatar": "http://www.47deg.com/assets/img/company/profile-juanpe.jpg", 96 | "name": "Juan Pedro Moreno", 97 | "twitter": "@juanpedromoreno" 98 | } 99 | }, 100 | { 101 | "name": "Forecast API Request", 102 | "description": "This example demonstrates how to call an endpoint with the use of Future, parse the json from the response and show the requested data in a view", 103 | "className": "com.fortysevendeg.scala.android.ui.apirequest.ForecastApiRequestActivity", 104 | "minApi": 14, 105 | "targetApi": 14, 106 | "scalaLevel": 2, 107 | "androidLevel": 1, 108 | "user" : { 109 | "avatar": "http://www.47deg.com/assets/img/company/profile-fede.jpg", 110 | "name": "Fede Fernández", 111 | "twitter": "@fede_fdz" 112 | } 113 | } 114 | ] 115 | -------------------------------------------------------------------------------- /src/main/res/drawable-hdpi/ic_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xebia-functional/scala-android/fa7222921dc79620e7410fab84b7cb4af86a19c2/src/main/res/drawable-hdpi/ic_add.png -------------------------------------------------------------------------------- /src/main/res/drawable-hdpi/icon_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xebia-functional/scala-android/fa7222921dc79620e7410fab84b7cb4af86a19c2/src/main/res/drawable-hdpi/icon_app.png -------------------------------------------------------------------------------- /src/main/res/drawable-mdpi/icon_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xebia-functional/scala-android/fa7222921dc79620e7410fab84b7cb4af86a19c2/src/main/res/drawable-mdpi/icon_app.png -------------------------------------------------------------------------------- /src/main/res/drawable-v21/background_default_fab.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 20 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/main/res/drawable-v21/background_default_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/main/res/drawable-xhdpi/ic_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xebia-functional/scala-android/fa7222921dc79620e7410fab84b7cb4af86a19c2/src/main/res/drawable-xhdpi/ic_add.png -------------------------------------------------------------------------------- /src/main/res/drawable-xhdpi/icon_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xebia-functional/scala-android/fa7222921dc79620e7410fab84b7cb4af86a19c2/src/main/res/drawable-xhdpi/icon_app.png -------------------------------------------------------------------------------- /src/main/res/drawable-xxhdpi/forecast_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xebia-functional/scala-android/fa7222921dc79620e7410fab84b7cb4af86a19c2/src/main/res/drawable-xxhdpi/forecast_01.png -------------------------------------------------------------------------------- /src/main/res/drawable-xxhdpi/forecast_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xebia-functional/scala-android/fa7222921dc79620e7410fab84b7cb4af86a19c2/src/main/res/drawable-xxhdpi/forecast_02.png -------------------------------------------------------------------------------- /src/main/res/drawable-xxhdpi/forecast_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xebia-functional/scala-android/fa7222921dc79620e7410fab84b7cb4af86a19c2/src/main/res/drawable-xxhdpi/forecast_03.png -------------------------------------------------------------------------------- /src/main/res/drawable-xxhdpi/forecast_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xebia-functional/scala-android/fa7222921dc79620e7410fab84b7cb4af86a19c2/src/main/res/drawable-xxhdpi/forecast_04.png -------------------------------------------------------------------------------- /src/main/res/drawable-xxhdpi/forecast_09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xebia-functional/scala-android/fa7222921dc79620e7410fab84b7cb4af86a19c2/src/main/res/drawable-xxhdpi/forecast_09.png -------------------------------------------------------------------------------- /src/main/res/drawable-xxhdpi/forecast_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xebia-functional/scala-android/fa7222921dc79620e7410fab84b7cb4af86a19c2/src/main/res/drawable-xxhdpi/forecast_10.png -------------------------------------------------------------------------------- /src/main/res/drawable-xxhdpi/forecast_11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xebia-functional/scala-android/fa7222921dc79620e7410fab84b7cb4af86a19c2/src/main/res/drawable-xxhdpi/forecast_11.png -------------------------------------------------------------------------------- /src/main/res/drawable-xxhdpi/forecast_13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xebia-functional/scala-android/fa7222921dc79620e7410fab84b7cb4af86a19c2/src/main/res/drawable-xxhdpi/forecast_13.png -------------------------------------------------------------------------------- /src/main/res/drawable-xxhdpi/forecast_50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xebia-functional/scala-android/fa7222921dc79620e7410fab84b7cb4af86a19c2/src/main/res/drawable-xxhdpi/forecast_50.png -------------------------------------------------------------------------------- /src/main/res/drawable-xxhdpi/ic_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xebia-functional/scala-android/fa7222921dc79620e7410fab84b7cb4af86a19c2/src/main/res/drawable-xxhdpi/ic_add.png -------------------------------------------------------------------------------- /src/main/res/drawable-xxhdpi/icon_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xebia-functional/scala-android/fa7222921dc79620e7410fab84b7cb4af86a19c2/src/main/res/drawable-xxhdpi/icon_app.png -------------------------------------------------------------------------------- /src/main/res/drawable-xxhdpi/map_marker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xebia-functional/scala-android/fa7222921dc79620e7410fab84b7cb4af86a19c2/src/main/res/drawable-xxhdpi/map_marker.png -------------------------------------------------------------------------------- /src/main/res/drawable-xxhdpi/unknown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xebia-functional/scala-android/fa7222921dc79620e7410fab84b7cb4af86a19c2/src/main/res/drawable-xxhdpi/unknown.png -------------------------------------------------------------------------------- /src/main/res/drawable-xxxhdpi/icon_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xebia-functional/scala-android/fa7222921dc79620e7410fab84b7cb4af86a19c2/src/main/res/drawable-xxxhdpi/icon_app.png -------------------------------------------------------------------------------- /src/main/res/drawable/background_default_fab.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 20 | 21 | 26 | 32 | 33 | -------------------------------------------------------------------------------- /src/main/res/drawable/background_default_fab_circle.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/res/drawable/background_default_fab_shadow.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 20 | 21 | -------------------------------------------------------------------------------- /src/main/res/drawable/background_default_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/res/drawable/background_error_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/res/drawable/background_item_api_advised.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/res/drawable/background_item_api_required.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/res/drawable/background_item_api_success.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/res/drawable/background_item_circle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/res/drawable/background_item_circle_activated.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/res/drawable/background_item_circle_pressed.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/res/drawable/background_item_icon_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/main/res/drawable/background_item_pathmorphsample_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/main/res/drawable/background_item_pathmorphsample_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/main/res/drawable/background_item_pathmorphsample_3.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/main/res/drawable/background_item_pathmorphsample_4.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/main/res/drawable/background_item_square.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/res/drawable/placeholder_circle.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/res/layout/image_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 20 | 21 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/main/res/layout/material_list_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 16 | 17 | 22 | 23 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/main/res/menu/activity_forecast.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /src/main/res/menu/activity_google_maps.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /src/main/res/values-v21/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #000000 4 | -------------------------------------------------------------------------------- /src/main/res/values-v21/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /src/main/res/values/app_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ${google.map.key} 4 | http://api.openweathermap.org/data/2.5/weather?lat=%1$s&lon=%2$s&units=metric 5 | x-api-key 6 | ${openweather.api.key} 7 | https://twitter.com/%s 8 | -------------------------------------------------------------------------------- /src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | #2D4053 6 | #ff192533 7 | #F2554A 8 | #ff49cb94 9 | 10 | #E4E9EB 11 | 12 | #ff49cb94 13 | #ff3da97c 14 | #46000000 15 | 16 | #11000000 17 | #22000000 18 | 19 | 20 | #F2554A 21 | #E5B838 22 | #18B491 23 | #ECF0F1 24 | #b92d4053 25 | #ffffff 26 | #802d4053 27 | #802d4053 28 | #E0E3E6 29 | 30 | 31 | #F44336 32 | #9C27B0 33 | #03A9F4 34 | 35 | 36 | #ffe6e6e6 37 | #ffd6d6d6 38 | #ffb6b6b6 39 | #ff868686 40 | #ffa6a6a6 41 | #ff696969 42 | #2E2E2E 43 | #545756 44 | #72979C 45 | #EDB55A 46 | 47 | 48 | #87364550 49 | #ffffff 50 | #E05F5E 51 | #ffc45453 52 | -------------------------------------------------------------------------------- /src/main/res/values/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | http://lorempixel.com/600/600/technics/ 4 | -------------------------------------------------------------------------------- /src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 3dp 6 | 2dp 7 | 4dp 8 | 8dp 9 | 8dp 10 | 16dp 11 | 12 | 2dp 13 | 4dp 14 | 15 | 1dp 16 | 17 | 10sp 18 | 12sp 19 | 14sp 20 | 16sp 21 | 20sp 22 | 24sp 23 | 24 | 56dp 25 | 26 | 27 | 32dp 28 | 60dp 29 | 30 | 31 | 2dp 32 | 64dp 33 | 34 | 200dp 35 | 36 | 37 | 132dp 38 | 48dp 39 | 40 | 41 | 240dp 42 | 64dp 43 | 44 | 45 | 24sp 46 | 60sp 47 | 18sp 48 | 14sp 49 | 160dp 50 | 51 | 52 | 200dp 53 | 54 | -------------------------------------------------------------------------------- /src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Scala API Demos 5 | Ripple Background 6 | Text Styles 7 | Circular Reveal 8 | Material Design List 9 | Path Morphing 10 | Select icon 11 | Select color 12 | Stroke: %1$s dp 13 | Size: %1$s x %2$s 14 | Google Maps 15 | 47 Degrees 16 | Reactive, scalable software solutions. 17 | Sample Marker 18 | This is a sample marker 19 | Satellite 20 | Normal 21 | Hybrid 22 | Terrain 23 | 24 | Scala: 25 | Android: 26 | 27 | Beginner 28 | Intermediate 29 | Advanced 30 | 31 | You need an Android version greater than 32 | API %s 33 | Add Marker 34 | Clear Map 35 | 36 | Activity not found 37 | JSON file not found 38 | Malformed JSON file 39 | An unexpected error has occurred 40 | 41 | Lorem ipsum dolor sit amet, vix vocent suscipit no. Definitionem delicatissimi at pri. Pro omnis civibus in, oporteat senserit instructior eos in. Ne usu tale definitionem, quo consetetur temporibus definitionem et, assum putent dolores mei in. No aliquam detraxit pro, quem eloquentiam id pro. 42 | Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Typi non habent claritatem insitam; est usus legentis in iis qui facit eorum claritatem. Investigationes demonstraverunt lectores legere me lius quod ii legunt saepius. Claritas est etiam processus dynamicus, qui sequitur mutationem consuetudium lectorum. Mirum est notare quam littera gothica, quam nunc putamus parum claram, anteposuerit litterarum formas humanitatis per seacula quarta decima et quinta decima. Eodem modo typi, qui nunc nobis videntur parum clari, fiant sollemnes in futurum. 43 | 44 | 45 | Akka Simon 46 | Welcome to\nAkka Simon 47 | Your Score\n%s 48 | Go! 49 | Round 50 | Round: %s 51 | 52 | 53 | Technics 54 | 55 | 56 | About 57 | 58 | The weather source of this demo is OpenWeatherMap 59 | \n\nAll data provided by OpenWeatherMap is distributed under terms of the Creative Commons license http://creativecommons.org/licenses/by-sa/2.0/. 60 | \n\n\nIcons created by Yannick (http://www.flaticon.com/authors/yannick) from http://www.flaticon.com. 61 | \n\nLicensed under Creative Commons BY 3.0 (http://creativecommons.org/licenses/by/3.0/) 62 | Forecast API Request 63 | Try again 64 | Waiting for a valid location 65 | Loading weather 66 | Too long? Try a sample location 67 | Unknown error 68 | Error getting location 69 | Error loading weather 70 | 71 | 72 | Add new item 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | -------------------------------------------------------------------------------- /src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 17 | -------------------------------------------------------------------------------- /src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | loggers = ["macroid.akka.AkkaAndroidLogger"] 3 | } -------------------------------------------------------------------------------- /src/main/scala/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xebia-functional/scala-android/fa7222921dc79620e7410fab84b7cb4af86a19c2/src/main/scala/.DS_Store -------------------------------------------------------------------------------- /src/main/scala/com/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xebia-functional/scala-android/fa7222921dc79620e7410fab84b7cb4af86a19c2/src/main/scala/com/.DS_Store -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xebia-functional/scala-android/fa7222921dc79620e7410fab84b7cb4af86a19c2/src/main/scala/com/fortysevendeg/.DS_Store -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xebia-functional/scala-android/fa7222921dc79620e7410fab84b7cb4af86a19c2/src/main/scala/com/fortysevendeg/scala/.DS_Store -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xebia-functional/scala-android/fa7222921dc79620e7410fab84b7cb4af86a19c2/src/main/scala/com/fortysevendeg/scala/android/.DS_Store -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/modules/forecast/Messages.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 47 Degrees, LLC http://47deg.com hello@47deg.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | * not use this file except in compliance with the License. You may obtain 6 | * a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.fortysevendeg.scala.android.modules.forecast 18 | 19 | import com.fortysevendeg.scala.android.modules.forecast.model.Forecast 20 | 21 | case class ForecastRequest(latitude: Double, longitude: Double) 22 | 23 | case class ForecastResponse(forecastMaybe: Option[Forecast]) -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/modules/forecast/impl/ForecastServices.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 47 Degrees, LLC http://47deg.com hello@47deg.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | * not use this file except in compliance with the License. You may obtain 6 | * a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.fortysevendeg.scala.android.modules.forecast.impl 18 | 19 | import java.io.InputStream 20 | 21 | import com.fortysevendeg.macroid.extras.ResourcesExtras._ 22 | import com.fortysevendeg.scala.android.R 23 | import com.fortysevendeg.scala.android.modules.forecast.model._ 24 | import com.fortysevendeg.scala.android.modules.forecast.{ForecastRequest, ForecastResponse} 25 | import com.fortysevendeg.scala.android.modules.utils.NetUtils 26 | import io.taig.communicator.response.Plain 27 | import io.taig.communicator.result.Parser 28 | import macroid.ContextWrapper 29 | import play.api.libs.json.Json 30 | 31 | import scala.concurrent.Future 32 | 33 | trait ApiReads { 34 | 35 | implicit val apiCloudsReads = Json.reads[ApiClouds] 36 | implicit val apiWindReads = Json.reads[ApiWind] 37 | implicit val apiWeatherReads = Json.reads[ApiWeather] 38 | implicit val apiMainReads = Json.reads[ApiMain] 39 | implicit val apiSysReads = Json.reads[ApiSys] 40 | implicit val apiCoordReads = Json.reads[ApiCoord] 41 | implicit val apiModelReads = Json.reads[ApiModel] 42 | 43 | } 44 | 45 | object JsonParser 46 | extends Parser[ApiModel] 47 | with ApiReads { 48 | 49 | override def parse(response: Plain, stream: InputStream): ApiModel = 50 | Json.parse(scala.io.Source.fromInputStream(stream).mkString).as[ApiModel] 51 | 52 | } 53 | 54 | trait ForecastServices 55 | extends NetUtils 56 | with Conversions { 57 | 58 | def loadJsonUrl(latitude: Double, longitude: Double)(implicit context: ContextWrapper): String = 59 | resGetString(R.string.openweather_url, latitude.toString, longitude.toString) 60 | 61 | def loadHeaderTuple(implicit context: ContextWrapper): (String, String) = 62 | (resGetString(R.string.openweather_key_name), resGetString(R.string.openweather_key_value)) 63 | 64 | def loadForecast(request: ForecastRequest)(implicit context: ContextWrapper): Future[ForecastResponse] = { 65 | import scala.concurrent.ExecutionContext.Implicits.global 66 | 67 | implicit val parser = JsonParser 68 | 69 | val result = loadJson[ApiModel](loadJsonUrl(request.latitude, request.longitude), Seq(loadHeaderTuple)) 70 | result.transform( 71 | response => ForecastResponse(Some(toForecast(response))), 72 | throwable => throwable) 73 | } 74 | } 75 | 76 | object ForecastServices extends ForecastServices 77 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/modules/forecast/model/ApiModel.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.modules.forecast.model 2 | 3 | case class ApiModel( 4 | id: Long, 5 | dt: Long, 6 | base: String, 7 | cod: Int, 8 | name: String, 9 | coord: ApiCoord, 10 | sys: ApiSys, 11 | main: ApiMain, 12 | weather: Seq[ApiWeather], 13 | wind: ApiWind, 14 | rain: Option[Map[String, Double]], 15 | clouds: ApiClouds) 16 | 17 | case class ApiCoord( 18 | lon: Double, 19 | lat: Double) 20 | 21 | case class ApiSys( 22 | message: Option[Double], 23 | country: Option[String], 24 | sunrise: Option[Long], 25 | sunset: Option[Long]) 26 | 27 | case class ApiMain( 28 | temp: Option[Double], 29 | temp_min: Option[Double], 30 | temp_max: Option[Double], 31 | pressure: Option[Double], 32 | sea_level: Option[Double], 33 | grnd_level: Option[Double], 34 | humidity: Option[Int]) 35 | 36 | case class ApiWeather( 37 | id: Int, 38 | main: String, 39 | description: String, 40 | icon: String) 41 | 42 | case class ApiWind( 43 | speed: Option[Double], 44 | deg: Option[Double]) 45 | 46 | case class ApiClouds( 47 | all: Option[Int]) -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/modules/forecast/model/Conversions.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.modules.forecast.model 2 | 3 | trait Conversions { 4 | 5 | def toForecast(apiModel: ApiModel) = 6 | Forecast( 7 | Location(apiModel.id, apiModel.name, apiModel.coord.lat, apiModel.coord.lon), 8 | apiModel.weather.headOption map (toWeather(_, apiModel.main.temp))) 9 | 10 | def toWeather(weather: ApiWeather, temperature: Option[Double]) = 11 | Weather(weather.id, weather.main, weather.description, weather.icon, temperature) 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/modules/forecast/model/Forecast.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.modules.forecast.model 2 | 3 | case class Forecast(location: Location, weather: Option[Weather]) 4 | 5 | case class Location(id: Long, name: String, latitude: Double, longitude: Double) 6 | 7 | case class Weather(id: Int, name: String, description: String, icon: String, temperature: Option[Double]) -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/modules/utils/NetUtils.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.modules.utils 2 | 3 | import com.squareup.okhttp.{Headers, OkHttpClient} 4 | import io.taig.communicator.result.Parser 5 | 6 | import scala.concurrent.Future 7 | 8 | trait NetUtils { 9 | 10 | def loadJson[T](url: String, headers: Seq[(String, String)] = Seq.empty)(implicit parser: Parser[T], client: OkHttpClient = new OkHttpClient()): Future[T] = { 11 | 12 | import io.taig.communicator._ 13 | 14 | import scala.concurrent.ExecutionContext.Implicits.global 15 | 16 | Request(url) 17 | .headers(toHeaders(headers)) 18 | .get() 19 | .parse[T] 20 | .transform(response => response.payload, throwable => throwable) 21 | } 22 | 23 | def toHeaders(headers: Seq[(String, String)]): Headers = { 24 | val headersBuilder = new Headers.Builder() 25 | headers map { header => 26 | headersBuilder.add(header._1, header._2) 27 | } 28 | headersBuilder.build() 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/akkasimon/AkkaSimonActivity.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.akkasimon 2 | 3 | import android.os.Bundle 4 | import android.support.v4.app.FragmentActivity 5 | import android.support.v7.app.AppCompatActivity 6 | import android.view.MenuItem 7 | import com.fortysevendeg.macroid.extras.TextTweaks._ 8 | import com.fortysevendeg.scala.android.R 9 | import com.fortysevendeg.scala.android.ui.akkasimon.actors.{ColorActor, ComputerActor} 10 | import com.fortysevendeg.scala.android.ui.akkasimon.util.FragmentEnum._ 11 | import macroid.Contexts 12 | import macroid.FullDsl._ 13 | import macroid.akka.AkkaActivity 14 | 15 | class AkkaSimonActivity 16 | extends AppCompatActivity 17 | with Contexts[FragmentActivity] 18 | with AkkaActivity 19 | with Layout { 20 | 21 | val actorSystemName = "simonsystem" 22 | 23 | var roundCounter = 1 24 | 25 | lazy val computer = actorSystem.actorOf(ComputerActor.props, "computer") 26 | lazy val green = actorSystem.actorOf(ColorActor.props, GREEN.toLower) 27 | lazy val red = actorSystem.actorOf(ColorActor.props, RED.toLower) 28 | lazy val blue = actorSystem.actorOf(ColorActor.props, BLUE.toLower) 29 | lazy val yellow = actorSystem.actorOf(ColorActor.props, YELLOW.toLower) 30 | 31 | override def onCreate(savedInstanceState: Bundle) = { 32 | super.onCreate(savedInstanceState) 33 | 34 | (computer, green, red, blue, yellow) 35 | 36 | setContentView(layout) 37 | 38 | toolBar map setSupportActionBar 39 | 40 | getSupportActionBar.setDisplayHomeAsUpEnabled(true) 41 | } 42 | 43 | override def onDestroy(): Unit = { 44 | super.onDestroy() 45 | actorSystem.shutdown() 46 | } 47 | 48 | override def onOptionsItemSelected(item: MenuItem): Boolean = { 49 | item.getItemId match { 50 | case android.R.id.home => { 51 | finish() 52 | false 53 | } 54 | } 55 | super.onOptionsItemSelected(item) 56 | } 57 | 58 | def gameOver(rounds: Int) = goToOptions(rounds) 59 | 60 | def resetRound() = { 61 | roundCounter = 1 62 | rounds <~ tvText(getString(R.string.simon_round_counter, roundCounter.toString)) 63 | } 64 | 65 | def nextRound() = { 66 | roundCounter += 1 67 | rounds <~ tvText(getString(R.string.simon_round_counter, roundCounter.toString)) 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/akkasimon/Layout.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.akkasimon 2 | 3 | import android.graphics.Color 4 | import android.support.v4.app.{Fragment, FragmentManager} 5 | import android.widget.{Button, FrameLayout, LinearLayout, TextView} 6 | import com.fortysevendeg.macroid.extras.FragmentExtras._ 7 | import com.fortysevendeg.macroid.extras.TextTweaks._ 8 | import com.fortysevendeg.macroid.extras.ToolbarTweaks._ 9 | import com.fortysevendeg.macroid.extras.ViewTweaks._ 10 | import com.fortysevendeg.macroid.extras.ResourcesExtras._ 11 | import com.fortysevendeg.scala.android.R 12 | import com.fortysevendeg.scala.android.ui.akkasimon.actors.ComputerActor.NewGame 13 | import com.fortysevendeg.scala.android.ui.akkasimon.fragments.ColorFragment._ 14 | import com.fortysevendeg.scala.android.ui.akkasimon.fragments.ComputerFragment._ 15 | import com.fortysevendeg.scala.android.ui.akkasimon.fragments.{ColorFragment, ComputerFragment} 16 | import com.fortysevendeg.scala.android.ui.akkasimon.util.FragmentEnum._ 17 | import com.fortysevendeg.scala.android.ui.commons.ToolbarLayout 18 | import macroid.FullDsl._ 19 | import macroid._ 20 | 21 | import scala.language.postfixOps 22 | 23 | trait Layout 24 | extends ToolbarLayout 25 | with IdGeneration 26 | with Styles { 27 | 28 | var btnStart = slot[Button] 29 | 30 | var optionsScreenLayout = slot[LinearLayout] 31 | 32 | var gameScreenLayout = slot[LinearLayout] 33 | 34 | var rounds = slot[TextView] 35 | 36 | var message = slot[TextView] 37 | 38 | def layout 39 | (implicit context: ActivityContextWrapper, 40 | managerContext: FragmentManagerContext[Fragment, FragmentManager]) = getUi( 41 | l[LinearLayout]( 42 | toolBarLayout <~ tbTitle(R.string.simon_title), 43 | l[FrameLayout]( 44 | f[ComputerFragment]. 45 | pass(nameComputerKey -> COMPUTER.toLower). 46 | framed(Id.computer, COMPUTER.toLower), 47 | optionsScreen, 48 | gameScreen 49 | ) <~ contentStyle 50 | ) <~ rootStyle 51 | ) 52 | 53 | def optionsScreen(implicit context: ActivityContextWrapper, 54 | managerContext: FragmentManagerContext[Fragment, FragmentManager]) = { 55 | l[LinearLayout]( 56 | w[TextView] <~ wire(message) <~ messageStyle, 57 | w[Button] <~ buttonsStyle <~ On.click(Ui { 58 | findFragmentById[ComputerFragment](Id.computer) map (_.actor.get ! NewGame) 59 | } ~ goToGame()) 60 | ) <~ optionsContentStyle <~ wire(optionsScreenLayout) 61 | } 62 | 63 | def gameScreen(implicit context: ActivityContextWrapper, 64 | managerContext: FragmentManagerContext[Fragment, FragmentManager]) = { 65 | l[LinearLayout]( 66 | w[TextView] <~ roundsStyle <~ wire(rounds), 67 | l[LinearLayout]( 68 | l[LinearLayout]( 69 | f[ColorFragment] 70 | .pass(colorKey -> Color.GREEN, nameColorKey -> GREEN.toLower) 71 | .framed(Id.green, GREEN.toLower) <~ rowStyle, 72 | f[ColorFragment] 73 | .pass(colorKey -> Color.RED, nameColorKey -> RED.toLower) 74 | .framed(Id.red, RED.toLower) <~ rowStyle 75 | ) <~ columnStyle, 76 | l[LinearLayout]( 77 | f[ColorFragment] 78 | .pass(colorKey -> Color.BLUE, nameColorKey -> BLUE.toLower) 79 | .framed(Id.blue, BLUE.toLower) <~ rowStyle, 80 | f[ColorFragment] 81 | .pass(colorKey -> Color.YELLOW, nameColorKey -> YELLOW.toLower) 82 | .framed(Id.yellow, YELLOW.toLower) <~ rowStyle 83 | ) <~ columnStyle 84 | ) <~ simonContainerStyle 85 | ) <~ gameContentStyle <~ wire(gameScreenLayout) 86 | } 87 | 88 | def goToGame() = (optionsScreenLayout <~ vGone) ~ (gameScreenLayout <~ vVisible) 89 | 90 | def goToOptions(rounds: Int)(implicit context: ContextWrapper) = { 91 | (optionsScreenLayout <~ vVisible) ~ 92 | (gameScreenLayout <~ vGone) ~ 93 | (message <~ tvText(resGetString(R.string.simon_rounds_message, rounds.toString))) 94 | } 95 | 96 | } -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/akkasimon/Styles.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.akkasimon 2 | 3 | import android.view.Gravity 4 | import android.widget.{TextView, Button, FrameLayout, LinearLayout} 5 | import com.fortysevendeg.macroid.extras.DeviceVersion.Lollipop 6 | import com.fortysevendeg.macroid.extras.LinearLayoutTweaks._ 7 | import com.fortysevendeg.macroid.extras.ResourcesExtras._ 8 | import com.fortysevendeg.macroid.extras.TextTweaks._ 9 | import com.fortysevendeg.macroid.extras.ViewTweaks._ 10 | import com.fortysevendeg.scala.android.R 11 | import macroid.FullDsl._ 12 | import macroid.{ContextWrapper, Tweak} 13 | 14 | import scala.language.postfixOps 15 | 16 | trait Styles { 17 | 18 | def simonButton(color: Int, alpha: Float = 0.3f)(implicit context: ContextWrapper) = 19 | vBackgroundColor(color) + 20 | vAlpha(alpha) 21 | 22 | val rootStyle: Tweak[LinearLayout] = llVertical 23 | 24 | val contentStyle: Tweak[FrameLayout] = llMatchWeightVertical 25 | 26 | val optionsContentStyle: Tweak[LinearLayout] = 27 | vMatchParent + 28 | llGravity(Gravity.CENTER) + 29 | llVertical 30 | 31 | def messageStyle(implicit context: ContextWrapper): Tweak[TextView] = 32 | vWrapContent + 33 | tvText(R.string.simon_welcome) + 34 | tvSizeResource(R.dimen.font_size_large) + 35 | tvAllCaps + 36 | tvGravity(Gravity.CENTER) + 37 | vPadding(paddingBottom = resGetDimensionPixelSize(R.dimen.padding_default_xlarge)) + 38 | tvNormalLight 39 | 40 | def buttonsStyle(implicit context: ContextWrapper): Tweak[Button] = { 41 | val size = resGetDimensionPixelSize(R.dimen.size_fab_default) 42 | lp[LinearLayout](size, size) + 43 | tvText(R.string.simon_start) + 44 | vBackground(R.drawable.background_default_fab) + 45 | (Lollipop ifSupportedThen vElevation(resGetDimension(R.dimen.padding_default_small)) getOrElse Tweak.blank) 46 | } 47 | 48 | val gameContentStyle: Tweak[LinearLayout] = 49 | vMatchParent + 50 | llVertical + 51 | vGone 52 | 53 | def roundsStyle(implicit context: ContextWrapper): Tweak[TextView] = 54 | vMatchWidth + 55 | tvGravity(Gravity.CENTER) + 56 | tvAllCaps + 57 | tvSizeResource(R.dimen.font_size_large) + 58 | vPaddings(resGetDimensionPixelSize(R.dimen.padding_default)) + 59 | tvNormalLight 60 | 61 | val simonContainerStyle: Tweak[LinearLayout] = 62 | llMatchWeightVertical + 63 | llHorizontal 64 | 65 | val columnStyle: Tweak[LinearLayout] = 66 | llMatchWeightHorizontal + 67 | llVertical 68 | 69 | val rowStyle: Tweak[FrameLayout] = llMatchWeightVertical 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/akkasimon/actors/ColorActor.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.akkasimon.actors 2 | 3 | import akka.actor.{ActorLogging, Props} 4 | import com.fortysevendeg.scala.android.ui.akkasimon.actors.ComputerActor.RoundItemActorColor 5 | import com.fortysevendeg.scala.android.ui.akkasimon.fragments.ColorFragment 6 | import macroid.Ui 7 | import macroid.akka.FragmentActor 8 | 9 | class ColorActor extends FragmentActor[ColorFragment] with ActorLogging { 10 | 11 | import com.fortysevendeg.scala.android.ui.akkasimon.actors.ColorActor._ 12 | import macroid.akka.FragmentActor._ 13 | 14 | def receive = receiveUi andThen { 15 | case LightOn(Nil) => 16 | withUi(f => f.receive) 17 | case LightOn(head :: tail) => 18 | withUi(f => f.receive ~~ Ui(head.actor ! LightOn(tail))) 19 | case AttachUi(_) => 20 | case DetachUi => 21 | } 22 | } 23 | 24 | object ColorActor { 25 | 26 | case class LightOn(game: List[RoundItemActorColor]) 27 | 28 | def props = Props(new ColorActor) 29 | } -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/akkasimon/actors/ComputerActor.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.akkasimon.actors 2 | 3 | import akka.actor.{ActorLogging, ActorSelection, Props} 4 | import com.fortysevendeg.scala.android.ui.akkasimon.AkkaSimonActivity 5 | import com.fortysevendeg.scala.android.ui.akkasimon.actors.ColorActor.LightOn 6 | import com.fortysevendeg.scala.android.ui.akkasimon.fragments.ComputerFragment 7 | import macroid.akka.FragmentActor 8 | 9 | import scala.collection.mutable.ArrayBuffer 10 | 11 | class ComputerActor extends FragmentActor[ComputerFragment] with ActorLogging { 12 | 13 | import com.fortysevendeg.scala.android.ui.akkasimon.actors.ComputerActor._ 14 | import macroid.akka.FragmentActor._ 15 | 16 | var gameList: List[RoundItemActorColor] = List.empty 17 | 18 | val receivedFromUser: ArrayBuffer[ClickedUserColor] = ArrayBuffer.empty 19 | 20 | def receive = receiveUi andThen { 21 | case NewGame => 22 | withUi(f => f.newGame) 23 | case GameOver => 24 | val rounds = gameList.length 25 | gameList = List.empty 26 | withUi(f => f.getActivity.asInstanceOf[AkkaSimonActivity].gameOver(rounds)) 27 | case ResetRound(round) => 28 | addRound(round) 29 | withUi(f => f.getActivity.asInstanceOf[AkkaSimonActivity].resetRound()) 30 | case NextRound(round) => 31 | addRound(round) 32 | withUi(f => f.getActivity.asInstanceOf[AkkaSimonActivity].nextRound()) 33 | case userEvent: ClickedUserColor => 34 | receivedFromUser += userEvent 35 | withUi(f => f.checkGame(gameList, receivedFromUser.toList map (_.color))) 36 | case AttachUi(_) => 37 | case DetachUi => 38 | } 39 | 40 | def addRound(round: RoundItemActorColor) = { 41 | gameList = gameList :+ round 42 | receivedFromUser.clear() 43 | gameList.head.actor ! LightOn(gameList.tail) 44 | } 45 | 46 | } 47 | 48 | object ComputerActor { 49 | 50 | case object NewGame 51 | 52 | case object GameOver 53 | 54 | case class ResetRound(round: RoundItemActorColor) 55 | 56 | case class NextRound(round: RoundItemActorColor) 57 | 58 | case class RoundItemActorColor(actor: ActorSelection, color: Int) 59 | 60 | case class ClickedUserColor(color: Int) 61 | 62 | def props = Props(new ComputerActor) 63 | } -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/akkasimon/fragments/ColorFragment.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.akkasimon.fragments 2 | 3 | import android.os.Bundle 4 | import android.view.{LayoutInflater, ViewGroup} 5 | import android.widget.Button 6 | import com.fortysevendeg.scala.android.ui.akkasimon.Styles 7 | import com.fortysevendeg.scala.android.ui.akkasimon.actors.ComputerActor.ClickedUserColor 8 | import com.fortysevendeg.scala.android.ui.akkasimon.util.SimonAkkaFragment 9 | import com.fortysevendeg.macroid.extras.ViewTweaks._ 10 | import macroid.FullDsl._ 11 | import macroid._ 12 | import ColorFragment._ 13 | 14 | import scala.concurrent.ExecutionContext.Implicits.global 15 | 16 | class ColorFragment 17 | extends SimonAkkaFragment 18 | with Styles { 19 | 20 | lazy val actorName = getArguments.getString(nameColorKey) 21 | lazy val color = getArguments.getInt(colorKey) 22 | 23 | lazy val actor = Some(actorSystem.actorSelection(s"/user/$actorName")) 24 | 25 | var simonColor = slot[Button] 26 | 27 | def receive = lightColor(color) 28 | 29 | def lightColor(c: Int = color) = simonColor <~ vAlpha(1f) <~~ delay(600) <~ simonButton(c) <~~ delay(600) 30 | 31 | override def onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle) = getUi { 32 | w[Button] <~ wire(simonColor) <~ simonButton(color) <~ On.click(lightColor() ~~ 33 | Ui(computerActor ! ClickedUserColor(color))) 34 | } 35 | } 36 | 37 | object ColorFragment { 38 | val nameColorKey = "name" 39 | val colorKey = "color" 40 | } -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/akkasimon/fragments/ComputerFragment.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.akkasimon.fragments 2 | 3 | import com.fortysevendeg.scala.android.ui.akkasimon.actors.ComputerActor._ 4 | import com.fortysevendeg.scala.android.ui.akkasimon.actors.ComputerActor.RoundItemActorColor 5 | import com.fortysevendeg.scala.android.ui.akkasimon.util.SimonAkkaFragment 6 | import macroid._ 7 | import ComputerFragment._ 8 | 9 | class ComputerFragment 10 | extends SimonAkkaFragment { 11 | 12 | lazy val actorName = getArguments.getString(nameComputerKey) 13 | 14 | lazy val actor = Some(actorSystem.actorSelection(s"/user/$actorName")) 15 | 16 | def newGame = Ui(computerActor ! ResetRound(newRound())) 17 | 18 | def checkGame(gameList: List[RoundItemActorColor], userClicks: List[Int]) = Ui { 19 | 20 | if (userClicks.size == gameList.size) { 21 | 22 | val colors = gameList map (_.color) 23 | 24 | if (userClicks.zip(colors).exists(t => t._1 != t._2)) 25 | computerActor ! GameOver 26 | else 27 | computerActor ! NextRound(newRound()) 28 | } 29 | } 30 | } 31 | 32 | object ComputerFragment { 33 | val nameComputerKey = "name" 34 | } 35 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/akkasimon/util/FragmentEnum.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.akkasimon.util 2 | 3 | object FragmentEnum { 4 | sealed trait FragmentEnumType 5 | 6 | case object GREEN extends FragmentEnumType 7 | case object RED extends FragmentEnumType 8 | case object BLUE extends FragmentEnumType 9 | case object YELLOW extends FragmentEnumType 10 | 11 | case object COMPUTER extends FragmentEnumType 12 | 13 | implicit class RichSimonColor(sc: FragmentEnumType) { 14 | def toLower = sc.toString.toLowerCase 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/akkasimon/util/SimonAkkaFragment.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.akkasimon.util 2 | 3 | import android.graphics.Color 4 | import com.fortysevendeg.scala.android.ui.akkasimon.actors.ComputerActor.RoundItemActorColor 5 | import com.fortysevendeg.scala.android.ui.akkasimon.util.FragmentEnum._ 6 | import macroid.Contexts 7 | import macroid.akka.AkkaFragment 8 | import scala.language.postfixOps 9 | import scala.util.Random 10 | 11 | trait SimonAkkaFragment 12 | extends AkkaFragment 13 | with Contexts[AkkaFragment] { 14 | 15 | val random = Random 16 | 17 | def customActorPath(actorName: String) = s"/user/$actorName" 18 | 19 | lazy val computerActor = actorSystem.actorSelection(customActorPath(COMPUTER.toLower)) 20 | 21 | lazy val greenActor = actorSystem.actorSelection(customActorPath(GREEN.toLower)) 22 | 23 | lazy val redActor = actorSystem.actorSelection(customActorPath(RED.toLower)) 24 | 25 | lazy val blueActor = actorSystem.actorSelection(customActorPath(BLUE.toLower)) 26 | 27 | lazy val yellowActor = actorSystem.actorSelection(customActorPath(YELLOW.toLower)) 28 | 29 | def newRound(): RoundItemActorColor = { 30 | val actorList = List( 31 | RoundItemActorColor(greenActor, Color.GREEN), 32 | RoundItemActorColor(redActor, Color.RED), 33 | RoundItemActorColor(blueActor, Color.BLUE), 34 | RoundItemActorColor(yellowActor, Color.YELLOW)) 35 | actorList(random.nextInt(4)) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/apirequest/ForecastApiRequestActivity.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.apirequest 2 | 3 | import android.app.AlertDialog 4 | import android.content.Context 5 | import android.location.{Criteria, Location, LocationListener, LocationManager} 6 | import android.os.Bundle 7 | import android.support.v4.app.FragmentActivity 8 | import android.support.v7.app.AppCompatActivity 9 | import android.text.util.Linkify 10 | import android.view.{Menu, MenuItem} 11 | import com.fortysevendeg.macroid.extras.FragmentExtras._ 12 | import com.fortysevendeg.scala.android.R 13 | import macroid.Contexts 14 | import macroid.FullDsl._ 15 | 16 | class ForecastApiRequestActivity 17 | extends AppCompatActivity 18 | with Contexts[FragmentActivity] 19 | with Layout 20 | with DefaultLocationListener { 21 | 22 | lazy val locationManager = this.getSystemService(Context.LOCATION_SERVICE).asInstanceOf[LocationManager] 23 | 24 | override def onCreate(savedInstanceState: Bundle) = { 25 | super.onCreate(savedInstanceState) 26 | 27 | setContentView(layout) 28 | 29 | toolBar map setSupportActionBar 30 | 31 | getSupportActionBar.setDisplayHomeAsUpEnabled(true) 32 | 33 | if (savedInstanceState == null) { 34 | runUi(addFragment(f[ForecastFragment], Some(Id.fragment), Some(forecastFragmentName))) 35 | } 36 | } 37 | 38 | def loadClientLocation = { 39 | val criteria = new Criteria() 40 | criteria.setAccuracy(Criteria.NO_REQUIREMENT) 41 | 42 | Option(locationManager.getBestProvider(criteria, true)) map { provider => 43 | val last = Option(locationManager.getLastKnownLocation(provider)) 44 | if (last.isDefined) loadForecast(last.get.getLatitude, last.get.getLongitude) 45 | else locationManager.requestLocationUpdates(provider, 0, 0, this) 46 | } 47 | } 48 | 49 | override def onCreateOptionsMenu(menu: Menu): Boolean = { 50 | getMenuInflater.inflate(R.menu.activity_forecast, menu) 51 | true 52 | } 53 | 54 | override def onOptionsItemSelected(item: MenuItem): Boolean = 55 | item.getItemId match { 56 | case R.id.about_icons => 57 | val dialogView = new AboutDialogLayout 58 | dialogView.textView map (Linkify.addLinks(_, Linkify.WEB_URLS)) 59 | val dialogBuilder = new AlertDialog.Builder(this) 60 | dialogBuilder.setView(dialogView.content) 61 | dialogBuilder.setPositiveButton(android.R.string.ok, null) 62 | dialogBuilder.show() 63 | true 64 | case android.R.id.home => 65 | finish() 66 | true 67 | case _ => super.onOptionsItemSelected(item) 68 | } 69 | 70 | override def onLocationChanged(location: Location) = { 71 | locationManager.removeUpdates(this) 72 | loadForecast(location.getLatitude, location.getLongitude) 73 | } 74 | 75 | override def onProviderDisabled(provider: String) = { 76 | locationManager.removeUpdates(this) 77 | showError(R.string.error_message_api_request_location_api) 78 | } 79 | 80 | private def showError(errorMessage: Int) = 81 | findFragmentByTag[ForecastFragment](forecastFragmentName) map (_.error(Some(errorMessage))) 82 | 83 | private def loadForecast(latitude: Double, longitude: Double) = 84 | findFragmentByTag[ForecastFragment](forecastFragmentName) map (_.loadForecast((latitude, longitude))) 85 | } 86 | 87 | trait DefaultLocationListener extends LocationListener { 88 | 89 | override def onLocationChanged(location: Location) = {} 90 | 91 | override def onProviderEnabled(provider: String) = {} 92 | 93 | override def onStatusChanged(provider: String, status: Int, extras: Bundle) = {} 94 | 95 | override def onProviderDisabled(provider: String) = {} 96 | } 97 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/apirequest/ForecastFragment.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.apirequest 2 | 3 | import java.text.DecimalFormat 4 | 5 | import android.graphics.drawable.Drawable 6 | import android.os.Bundle 7 | import android.support.v4.app.Fragment 8 | import android.view.{LayoutInflater, View, ViewGroup} 9 | import com.fortysevendeg.macroid.extras.ImageViewTweaks._ 10 | import com.fortysevendeg.macroid.extras.ResourcesExtras._ 11 | import com.fortysevendeg.macroid.extras.TextTweaks._ 12 | import com.fortysevendeg.macroid.extras.ViewTweaks._ 13 | import com.fortysevendeg.scala.android.R 14 | import com.fortysevendeg.scala.android.modules.forecast.ForecastRequest 15 | import com.fortysevendeg.scala.android.modules.forecast.impl.ForecastServices 16 | import com.fortysevendeg.scala.android.modules.forecast.model.{Weather, Forecast} 17 | import macroid.FullDsl._ 18 | import macroid.{ContextWrapper, Contexts, Ui} 19 | 20 | import scala.concurrent.ExecutionContext.Implicits.global 21 | 22 | class ForecastFragment 23 | extends Fragment 24 | with Contexts[Fragment] { 25 | 26 | private var fragmentLayout: Option[ForecastFragmentLayout] = None 27 | 28 | val decimalFormatter = new DecimalFormat("#.##'°'") 29 | 30 | val resourceName = "forecast_%s" 31 | 32 | val placeholderTemperature = "-°" 33 | 34 | val iconSizeIdentifier = 2 35 | 36 | val sampleLocationProvider = "fake_provider" 37 | 38 | val sampleLocationLatitude = 47.6632164 39 | 40 | val sampleLocationLongitude = -122.3842024 41 | 42 | override def onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle): View = { 43 | 44 | val fLayout = new ForecastFragmentLayout 45 | 46 | fragmentLayout = Some(fLayout) 47 | 48 | runUi( 49 | (fLayout.reloadButton <~ On.click(Ui { reload })) ~ 50 | (fLayout.progressButton <~ On.click(Ui { loadSampleLocation })) 51 | ) 52 | 53 | fLayout.layout 54 | } 55 | 56 | override def onViewCreated(view: View, savedInstanceState: Bundle) = { 57 | super.onViewCreated(view, savedInstanceState) 58 | reload 59 | } 60 | 61 | def reload = { 62 | loadingForLocation 63 | Option(getActivity) map (_.asInstanceOf[ForecastApiRequestActivity].loadClientLocation) 64 | } 65 | 66 | def loadSampleLocation = { 67 | val location = new android.location.Location(sampleLocationProvider) 68 | location.setLatitude(sampleLocationLatitude) 69 | location.setLongitude(sampleLocationLongitude) 70 | Option(getActivity) map (_.asInstanceOf[ForecastApiRequestActivity].onLocationChanged(location)) 71 | } 72 | 73 | def loadForecast(location: (Double, Double)) = { 74 | loadingForForecast 75 | 76 | val result = for { 77 | forecast <- ForecastServices.loadForecast(ForecastRequest(location._1, location._2)) 78 | } yield forecast.forecastMaybe 79 | 80 | result map { 81 | case Some(forecast) => showForecast(forecast) 82 | case _ => error(Some(R.string.error_message_api_request_loading)) 83 | } recover { 84 | case _ => error(Some(R.string.error_message_api_request_loading)) 85 | } 86 | } 87 | 88 | def showForecast(forecast: Forecast) = 89 | fragmentLayout map { layout => 90 | runUi( 91 | (layout.progressContent <~ vGone) ~ 92 | (layout.errorContent <~ vGone) ~ 93 | (layout.detailLayoutContent <~ vVisible) ~ 94 | (layout.locationTextView <~ tvText(forecast.location.name)) ~ 95 | (layout.forecastImageView <~ ivSrc(loadWeatherIcon(forecast.weather))) ~ 96 | (layout.temperatureTextView <~ tvText(loadWeatherTemperature(forecast.weather))) 97 | ) 98 | } 99 | 100 | def loadWeatherIcon(weatherMaybe: Option[Weather]): Drawable = { 101 | val result = weatherMaybe flatMap { weather => 102 | Option(weather.icon) match { 103 | case Some(icon) if icon.length >= iconSizeIdentifier => resGetDrawable(String.format(resourceName, icon.substring(0, iconSizeIdentifier))) 104 | case _ => Option(resGetDrawable(R.drawable.unknown)) 105 | } 106 | } 107 | result getOrElse resGetDrawable(R.drawable.unknown) 108 | } 109 | 110 | def loadWeatherTemperature(weatherMaybe: Option[Weather]): String = { 111 | (for { 112 | weather <- weatherMaybe 113 | temperature <- weather.temperature 114 | } yield decimalFormatter.format(temperature)) getOrElse placeholderTemperature 115 | } 116 | 117 | def loadingForForecast = 118 | fragmentLayout map { layout => 119 | runUi( 120 | (layout.progressContent <~ vVisible) ~ 121 | (layout.progressText <~ tvText(R.string.loading_forecast_message)) ~ 122 | (layout.progressButton <~ vGone) ~ 123 | (layout.errorContent <~ vGone) ~ 124 | (layout.detailLayoutContent <~ vGone) 125 | ) 126 | } 127 | 128 | def loadingForLocation = 129 | fragmentLayout map { layout => 130 | runUi( 131 | (layout.progressContent <~ vVisible) ~ 132 | (layout.progressText <~ tvText(R.string.loading_location_message)) ~ 133 | (layout.progressButton <~ tvText(R.string.try_sample_location_button) <~ vVisible) ~ 134 | (layout.errorContent <~ vGone) ~ 135 | (layout.detailLayoutContent <~ vGone) 136 | ) 137 | } 138 | 139 | def error(errorMessage: Option[Int]) = 140 | fragmentLayout map { layout => 141 | runUi( 142 | (layout.progressContent <~ vGone) ~ 143 | (layout.errorContent <~ vVisible) ~ 144 | (layout.errorText <~ tvText(errorMessage getOrElse R.string.error_message_api_request_default)) ~ 145 | (layout.detailLayoutContent <~ vGone) 146 | ) 147 | } 148 | 149 | } -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/apirequest/Layout.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.apirequest 2 | 3 | import android.support.v4.app.{Fragment, FragmentManager} 4 | import android.widget._ 5 | import com.fortysevendeg.macroid.extras.TextTweaks._ 6 | import com.fortysevendeg.macroid.extras.ToolbarTweaks._ 7 | import com.fortysevendeg.scala.android.R 8 | import com.fortysevendeg.scala.android.ui.apirequest.Styles._ 9 | import com.fortysevendeg.scala.android.ui.commons.ToolbarLayout 10 | import macroid.FullDsl._ 11 | import macroid.{ActivityContextWrapper, ContextWrapper, FragmentManagerContext, IdGeneration} 12 | 13 | trait Layout extends ToolbarLayout with IdGeneration { 14 | 15 | val loaderFragmentName = Tag.fragmentLoader 16 | 17 | val forecastFragmentName = Tag.fragmentForecast 18 | 19 | var content = slot[FrameLayout] 20 | 21 | def layout(implicit context: ActivityContextWrapper, 22 | managerContext: FragmentManagerContext[Fragment, FragmentManager]) = getUi( 23 | l[LinearLayout]( 24 | toolBarLayout <~ tbTitle(R.string.title_forecast_api_request), 25 | l[FrameLayout]() <~ wire(content) <~ id(Id.fragment) <~ contentStyle 26 | ) <~ rootStyle 27 | ) 28 | 29 | } 30 | 31 | trait ErrorLayout { 32 | 33 | var progressContent = slot[LinearLayout] 34 | 35 | var progressBar = slot[ProgressBar] 36 | 37 | var progressText = slot[TextView] 38 | 39 | var progressButton = slot[Button] 40 | 41 | var errorContent = slot[LinearLayout] 42 | 43 | var reloadButton = slot[Button] 44 | 45 | var errorText = slot[TextView] 46 | 47 | def layoutView(implicit context: ActivityContextWrapper) = 48 | l[FrameLayout]( 49 | l[LinearLayout]( 50 | w[ProgressBar] <~ wire(progressBar) <~ progressBarStyle, 51 | w[TextView] <~ wire(progressText) <~ messageStyle, 52 | w[Button] <~ wire(progressButton) <~ buttonStyle 53 | ) <~ wire(progressContent) <~ progressContentStyle, 54 | l[LinearLayout]( 55 | w[TextView] <~ wire(errorText) <~ messageStyle, 56 | w[Button] <~ wire(reloadButton) <~ tvText(R.string.try_again_button) <~ buttonStyle 57 | ) <~ wire(errorContent) <~ errorContentStyle 58 | ) <~ errorLayoutStyle 59 | 60 | } 61 | 62 | class ForecastFragmentLayout(implicit context: ActivityContextWrapper) extends ErrorLayout { 63 | 64 | var errorLayoutContent = slot[FrameLayout] 65 | 66 | var detailLayoutContent = slot[LinearLayout] 67 | 68 | var locationTextView = slot[TextView] 69 | 70 | var forecastImageView = slot[ImageView] 71 | 72 | var temperatureTextView = slot[TextView] 73 | 74 | val content = getUi( 75 | l[FrameLayout]( 76 | layoutView <~ wire(errorLayoutContent), 77 | l[LinearLayout]( 78 | l[LinearLayout]( 79 | w[ImageView] <~ markerImageViewStyle, 80 | w[TextView] <~ wire(locationTextView) <~ locationTextViewStyle 81 | ) <~ forecastLocationLayoutStyle, 82 | l[LinearLayout]( 83 | w[ImageView] <~ wire(forecastImageView) <~ forecastImageViewStyle, 84 | w[TextView] <~ wire(temperatureTextView) <~ temperatureTextViewStyle 85 | ) <~ forecastDetailLayoutStyle 86 | ) <~ wire(detailLayoutContent) <~ forecastLayoutStyle 87 | ) <~ forecastFragmentLayoutStyle 88 | ) 89 | 90 | def layout = content 91 | 92 | } 93 | 94 | class AboutDialogLayout(implicit context: ActivityContextWrapper) { 95 | 96 | var textView = slot[TextView] 97 | 98 | val content = getUi( 99 | l[FrameLayout]( 100 | w[TextView] <~ wire(textView) <~ dialogTextViewStyle 101 | ) <~ aboutDialogLayoutStyle 102 | ) 103 | 104 | def layout = content 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/apirequest/Styles.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.apirequest 2 | 3 | import android.view.Gravity 4 | import android.widget.{ImageView, ProgressBar, LinearLayout, TextView} 5 | import com.fortysevendeg.macroid.extras.FrameLayoutTweaks._ 6 | import com.fortysevendeg.macroid.extras.LinearLayoutTweaks._ 7 | import com.fortysevendeg.macroid.extras.TextTweaks._ 8 | import com.fortysevendeg.macroid.extras.ImageViewTweaks._ 9 | import com.fortysevendeg.macroid.extras.ViewTweaks._ 10 | import com.fortysevendeg.macroid.extras.ResourcesExtras._ 11 | import com.fortysevendeg.scala.android.R 12 | import macroid.{ContextWrapper, Tweak} 13 | 14 | import scala.language.postfixOps 15 | 16 | object Styles { 17 | 18 | val rootStyle = llVertical 19 | 20 | val contentStyle = vMatchParent 21 | 22 | val errorLayoutStyle = vMatchParent 23 | 24 | val errorContentStyle: Tweak[LinearLayout] = 25 | vWrapContent + 26 | flLayoutGravity(Gravity.CENTER) + 27 | llGravity(Gravity.CENTER_HORIZONTAL) + 28 | llVertical + 29 | vGone 30 | 31 | val progressContentStyle: Tweak[LinearLayout] = 32 | vWrapContent + 33 | flLayoutGravity(Gravity.CENTER) + 34 | llGravity(Gravity.CENTER_HORIZONTAL) + 35 | llVertical + 36 | vGone 37 | 38 | val progressBarStyle: Tweak[ProgressBar] = 39 | vWrapContent + 40 | flLayoutGravity(Gravity.CENTER) 41 | 42 | def messageStyle(implicit context: ContextWrapper): Tweak[TextView] = 43 | vWrapContent + 44 | tvGravity(Gravity.CENTER) + 45 | tvColorResource(R.color.text_error_message) + 46 | tvSizeResource(R.dimen.text_size_forecast_error_message) + 47 | vPaddings(resGetDimensionPixelSize(R.dimen.padding_default)) 48 | 49 | def buttonStyle(implicit context: ContextWrapper): Tweak[TextView] = 50 | vWrapContent + 51 | vMinWidth(resGetDimensionPixelSize(R.dimen.width_forecast_error_button)) + 52 | tvColorResource(R.color.text_error_button) + 53 | vBackground(R.drawable.background_error_button) + 54 | tvAllCaps + 55 | tvSizeResource(R.dimen.text_size_forecast_error_button) + 56 | tvGravity(Gravity.CENTER) 57 | 58 | val forecastFragmentLayoutStyle = vMatchParent 59 | 60 | def forecastLayoutStyle(implicit context: ContextWrapper): Tweak[LinearLayout] = 61 | vMatchParent + 62 | llVertical + 63 | vPaddings(resGetDimensionPixelSize(R.dimen.padding_default)) 64 | 65 | def forecastLocationLayoutStyle(implicit context: ContextWrapper): Tweak[LinearLayout] = 66 | vMatchWidth + 67 | llHorizontal + 68 | llGravity(Gravity.CENTER_VERTICAL) 69 | 70 | def forecastDetailLayoutStyle(implicit context: ContextWrapper): Tweak[LinearLayout] = 71 | vMatchParent + 72 | llVertical + 73 | llGravity(Gravity.CENTER) 74 | 75 | def markerImageViewStyle(implicit context: ContextWrapper): Tweak[ImageView] = 76 | vWrapContent + 77 | vPaddings(resGetDimensionPixelSize(R.dimen.padding_default)) + 78 | ivSrc(R.drawable.map_marker) 79 | 80 | def locationTextViewStyle(implicit context: ContextWrapper) = 81 | vWrapContent + 82 | tvSizeResource(R.dimen.text_size_forecast_location) + 83 | vPaddings(resGetDimensionPixelSize(R.dimen.padding_default)) 84 | 85 | val forecastImageViewStyle = vWrapContent 86 | 87 | def temperatureTextViewStyle(implicit context: ContextWrapper) = 88 | vWrapContent + 89 | tvGravity(Gravity.CENTER) + 90 | tvSizeResource(R.dimen.text_size_forecast_temperature) + 91 | tvBoldCondensed + 92 | vPaddings(resGetDimensionPixelSize(R.dimen.padding_default)) 93 | 94 | def aboutDialogLayoutStyle(implicit context: ContextWrapper) = 95 | vMatchParent + 96 | vPaddings(resGetDimensionPixelSize(R.dimen.padding_default)) 97 | 98 | val dialogTextViewStyle = 99 | vWrapContent + 100 | tvText(R.string.forecast_attribution) 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/circularreveal/CircularRevealActivity.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.circularreveal 2 | 3 | import android.os.Bundle 4 | import android.support.v4.app.{Fragment, FragmentActivity} 5 | import android.support.v7.app.AppCompatActivity 6 | import android.view.MenuItem 7 | import com.fortysevendeg.scala.android.ui.components.IconTypes._ 8 | import com.fortysevendeg.scala.android.ui.components.PathMorphDrawableTweaks._ 9 | import macroid.{Ui, Contexts} 10 | import com.fortysevendeg.macroid.extras.FragmentExtras._ 11 | import macroid.FullDsl._ 12 | 13 | class CircularRevealActivity 14 | extends AppCompatActivity 15 | with Contexts[FragmentActivity] 16 | with Layout { 17 | 18 | override def onCreate(savedInstanceState: Bundle) = { 19 | super.onCreate(savedInstanceState) 20 | 21 | setContentView(layout) 22 | 23 | toolBar foreach setSupportActionBar 24 | 25 | getSupportActionBar.setDisplayHomeAsUpEnabled(true) 26 | 27 | } 28 | 29 | def remove(fragment: Fragment): Unit = removeFragment(fragment) 30 | 31 | override def onOptionsItemSelected(item: MenuItem): Boolean = item.getItemId match { 32 | case android.R.id.home => 33 | finish() 34 | false 35 | case _ => super.onOptionsItemSelected(item) 36 | } 37 | 38 | override def onBackPressed(): Unit = 39 | findFragmentByTag[SampleFragment](fragmentName) match { 40 | case Some(f) => 41 | runUi(Ui(f.unreveal()) ~ 42 | (circleButton <~ pmdAnimIcon(ADD))) 43 | case _ => super.onBackPressed() 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/circularreveal/Layout.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.circularreveal 2 | 3 | import android.os.Bundle 4 | import android.support.v4.app.{FragmentManager, Fragment} 5 | import android.support.v7.widget.CardView 6 | import android.widget.{ImageView, TextView, FrameLayout, LinearLayout} 7 | import com.fortysevendeg.macroid.extras.ResourcesExtras._ 8 | import com.fortysevendeg.scala.android.R 9 | import com.fortysevendeg.macroid.extras.ToolbarTweaks._ 10 | import com.fortysevendeg.scala.android.ui.commons.ToolbarLayout 11 | import macroid.FullDsl._ 12 | import macroid._ 13 | import com.fortysevendeg.macroid.extras.FragmentExtras._ 14 | import com.fortysevendeg.scala.android.ui.components.PathMorphDrawableTweaks._ 15 | import com.fortysevendeg.scala.android.ui.components.IconTypes._ 16 | 17 | trait Layout 18 | extends ToolbarLayout 19 | with IdGeneration 20 | with Styles { 21 | 22 | val fragmentName = "sample-fragment" 23 | 24 | var circleButton = slot[ImageView] 25 | 26 | var content = slot[FrameLayout] 27 | 28 | def layout(implicit context: ActivityContextWrapper, 29 | managerContext: FragmentManagerContext[Fragment, FragmentManager]) = getUi( 30 | l[LinearLayout]( 31 | toolBarLayout <~ tbTitle(R.string.title_circular_reveal_styles), 32 | l[FrameLayout]( 33 | l[FrameLayout]() <~ wire(content) <~ id(Id.fragment) <~ fragmentStyle, 34 | w[ImageView] <~ wire(circleButton) <~ fabStyle <~ On.Click { 35 | findFragmentByTag[SampleFragment](fragmentName) match { 36 | case Some(f) => 37 | Ui(f.unreveal()) ~ 38 | (circleButton <~ pmdAnimIcon(ADD)) 39 | case _ => 40 | val margin = resGetDimensionPixelSize(R.dimen.padding_default) 41 | val (x: Int, y: Int) = (for { 42 | circle <- circleButton 43 | c <- content 44 | } yield (circle.getLeft - margin - c.getLeft + circle.getWidth / 2, 45 | circle.getTop - margin - c.getTop + (circle.getHeight / 2))).getOrElse(0, 0) 46 | val args = new Bundle() 47 | args.putInt(SampleFragment.posX, x) 48 | args.putInt(SampleFragment.posY, y) 49 | addFragment(f[SampleFragment].pass(args), Some(Id.fragment), Some(fragmentName)) ~ 50 | (circleButton <~ pmdAnimIcon(CLOSE)) 51 | } 52 | } 53 | ) <~ contentStyle 54 | ) <~ rootStyle 55 | ) 56 | 57 | } 58 | 59 | class FragmentLayout(implicit context: ActivityContextWrapper) 60 | extends FragmentStyles { 61 | 62 | val content = getUi( 63 | l[CardView]( 64 | l[LinearLayout]( 65 | w[ImageView] <~ imageStyle, 66 | w[TextView] <~ textTitleStyle, 67 | w[TextView] <~ textMessageStyle 68 | ) <~ contentLayoutStyle 69 | ) <~ contentRevealStyle 70 | ) 71 | 72 | def layout = content 73 | 74 | } -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/circularreveal/SampleFragment.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.circularreveal 2 | 3 | import android.animation.{AnimatorListenerAdapter, ObjectAnimator, Animator} 4 | import android.annotation.TargetApi 5 | import android.os.Build.VERSION 6 | import android.os.{Build, Bundle} 7 | import android.support.v4.app.Fragment 8 | import android.view.animation.{AccelerateInterpolator, DecelerateInterpolator} 9 | import android.view.{ViewAnimationUtils, View, ViewGroup, LayoutInflater} 10 | import com.fortysevendeg.macroid.extras.SnailsUtils 11 | import com.fortysevendeg.macroid.extras.DeviceVersion._ 12 | import macroid.Contexts 13 | 14 | class SampleFragment 15 | extends Fragment 16 | with Contexts[Fragment] { 17 | 18 | private var lastWidth: Option[Int] = None 19 | 20 | private var lastHeight: Option[Int] = None 21 | 22 | private var lastRevealX: Option[Int] = None 23 | 24 | private var lastRevealY: Option[Int] = None 25 | 26 | private var fragmentLayout: Option[FragmentLayout] = None 27 | 28 | override def onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle): View = { 29 | 30 | val fLayout = new FragmentLayout 31 | 32 | fragmentLayout = Some(fLayout) 33 | 34 | fLayout.layout.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { 35 | override def onLayoutChange(v: View, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int): Unit = { 36 | v.removeOnLayoutChangeListener(this) 37 | reveal(right - left, bottom - top) 38 | } 39 | }) 40 | 41 | fLayout.layout 42 | 43 | } 44 | 45 | def reveal(width: Int, height: Int) = { 46 | lastWidth = Some(width) 47 | lastHeight = Some(height) 48 | fragmentLayout.map { 49 | fLayout => 50 | CurrentVersion match { 51 | case version if version >= Lollipop => 52 | getLollipopReveal(fLayout.layout, width, height) 53 | case _ => 54 | ltLollipopReveal(fLayout.layout) 55 | } 56 | } 57 | } 58 | 59 | def unreveal(): Unit = { 60 | fragmentLayout.map { 61 | fLayout => 62 | CurrentVersion match { 63 | case version if version >= Lollipop => 64 | getLollipopUnreveal(fLayout.layout) 65 | case _ => 66 | ltLollipopUnreveal(fLayout.layout) 67 | } 68 | } 69 | } 70 | 71 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 72 | def getLollipopReveal(layout: View, width: Int, height: Int) = { 73 | val cx = getArguments.getInt(SampleFragment.posX, 0) 74 | val cy = getArguments.getInt(SampleFragment.posY, 0) 75 | lastRevealX = Some(cx) 76 | lastRevealY = Some(cy) 77 | val endRadius = SnailsUtils.calculateRadius(cx, cy, width, height) 78 | val reveal: Animator = ViewAnimationUtils.createCircularReveal(layout, cx, cy, 0, endRadius) 79 | reveal.setInterpolator(new DecelerateInterpolator(2f)) 80 | reveal.start() 81 | } 82 | 83 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 84 | def getLollipopUnreveal(layout: View) = { 85 | val (x, y, w, h) = (for { 86 | x <- lastRevealX 87 | y <- lastRevealY 88 | w <- lastWidth 89 | h <- lastHeight 90 | } yield (x, y, w, h)) getOrElse(0, 0, 0, 0) 91 | val radius = SnailsUtils.calculateRadius(x, y, w, h) 92 | val reveal: Animator = ViewAnimationUtils.createCircularReveal(layout, x, y, radius, 0) 93 | reveal.setInterpolator(new DecelerateInterpolator(2f)) 94 | reveal.addListener(new AnimatorListenerAdapter { 95 | override def onAnimationEnd(animation: Animator): Unit = { 96 | super.onAnimationEnd(animation) 97 | layout.setVisibility(View.GONE) 98 | getActivity.asInstanceOf[CircularRevealActivity].remove(SampleFragment.this) 99 | } 100 | }) 101 | reveal.start() 102 | } 103 | 104 | def ltLollipopReveal(layout: View) = { 105 | val alpha: Animator = ObjectAnimator.ofFloat(layout, "alpha", 0f, 1) 106 | alpha.setInterpolator(new DecelerateInterpolator(2f)) 107 | alpha.start() 108 | } 109 | 110 | def ltLollipopUnreveal(layout: View) = { 111 | val alpha: Animator = ObjectAnimator.ofFloat(layout, "alpha", 1f, 0f) 112 | alpha.setInterpolator(new AccelerateInterpolator(2f)) 113 | alpha.addListener(new AnimatorListenerAdapter { 114 | override def onAnimationEnd(animation: Animator): Unit = { 115 | super.onAnimationEnd(animation) 116 | layout.setVisibility(View.GONE) 117 | getActivity.asInstanceOf[CircularRevealActivity].remove(SampleFragment.this) 118 | } 119 | }) 120 | alpha.start() 121 | } 122 | 123 | } 124 | 125 | object SampleFragment { 126 | 127 | val posX = "pos_x" 128 | val posY = "pos_y" 129 | 130 | } 131 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/circularreveal/Styles.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.circularreveal 2 | 3 | import android.graphics.Color 4 | import android.support.v7.widget.CardView 5 | import android.view.Gravity 6 | import android.view.ViewGroup.LayoutParams._ 7 | import android.widget.ImageView.ScaleType 8 | import android.widget.{FrameLayout, ImageView, LinearLayout, TextView} 9 | import com.fortysevendeg.macroid.extras.DeviceVersion._ 10 | import com.fortysevendeg.macroid.extras.FrameLayoutTweaks._ 11 | import com.fortysevendeg.macroid.extras.LinearLayoutTweaks._ 12 | import com.fortysevendeg.macroid.extras.ResourcesExtras._ 13 | import com.fortysevendeg.macroid.extras.TextTweaks._ 14 | import com.fortysevendeg.macroid.extras.ImageViewTweaks._ 15 | import com.fortysevendeg.macroid.extras.ViewTweaks._ 16 | import com.fortysevendeg.scala.android.R 17 | import com.fortysevendeg.scala.android.ui.components.IconTypes._ 18 | import com.fortysevendeg.scala.android.ui.components.PathMorphDrawable 19 | import macroid.FullDsl._ 20 | import macroid.{ActivityContextWrapper, ContextWrapper, Tweak} 21 | import com.fortysevendeg.scala.android.ui.commons.AsyncImageTweaks._ 22 | 23 | import scala.language.postfixOps 24 | 25 | trait Styles { 26 | 27 | val rootStyle = llVertical 28 | 29 | def contentStyle(implicit context: ContextWrapper): Tweak[FrameLayout] = 30 | vMatchParent + 31 | vPaddings(resGetDimensionPixelSize(R.dimen.padding_default)) 32 | 33 | def fragmentStyle(implicit context: ContextWrapper): Tweak[FrameLayout] = vMatchParent 34 | 35 | def fabStyle(implicit context: ContextWrapper): Tweak[ImageView] = { 36 | val size = resGetDimensionPixelSize(R.dimen.size_fab_default) 37 | lp[FrameLayout](size, size) + 38 | flLayoutGravity(Gravity.RIGHT | Gravity.BOTTOM) + 39 | vBackground(R.drawable.background_default_fab) + 40 | vPaddings(resGetDimensionPixelSize(R.dimen.padding_default_xlarge)) + 41 | ivSrc(new PathMorphDrawable( 42 | defaultIcon = ADD, 43 | defaultStroke = resGetDimensionPixelSize(R.dimen.circular_reveal_fab_stroke), 44 | defaultColor = Color.WHITE 45 | )) + 46 | vMargins(resGetDimensionPixelSize(R.dimen.padding_default)) + 47 | (Lollipop ifSupportedThen vElevation(resGetDimension(R.dimen.padding_default_small)) getOrElse Tweak.blank) 48 | } 49 | 50 | } 51 | 52 | trait FragmentStyles { 53 | 54 | def contentRevealStyle(implicit context: ContextWrapper): Tweak[CardView] = 55 | vMatchParent 56 | 57 | val contentLayoutStyle: Tweak[LinearLayout] = 58 | vMatchParent + 59 | llVertical 60 | 61 | def imageStyle(implicit context: ActivityContextWrapper): Tweak[ImageView] = 62 | lp[LinearLayout](MATCH_PARENT, resGetDimensionPixelSize(R.dimen.circular_reveal_height_image)) + 63 | srcImage(resGetString(R.string.material_list_url_image)) + 64 | ivScaleType(ScaleType.CENTER_CROP) 65 | 66 | def textTitleStyle(implicit context: ContextWrapper): Tweak[TextView] = 67 | tvText(R.string.circular_reveal_title) + 68 | tvSizeResource(R.dimen.font_size_large) + 69 | tvText(R.string.circular_reveal_title) + 70 | vPaddings(resGetDimensionPixelSize(R.dimen.padding_default)) 71 | 72 | def textMessageStyle(implicit context: ContextWrapper): Tweak[TextView] = 73 | tvText(R.string.circular_reveal_title) + 74 | tvSizeResource(R.dimen.font_size_normal) + 75 | tvNormalLight + 76 | tvText(R.string.lorem_ipsum_large) + 77 | vPaddings(resGetDimensionPixelSize(R.dimen.padding_default)) 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/commons/CommonsStyles.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.commons 2 | 3 | import com.fortysevendeg.scala.android.R 4 | import com.fortysevendeg.macroid.extras.ViewTweaks._ 5 | 6 | object CommonsStyles { 7 | 8 | val toolbarStyle = 9 | vBackground(R.color.primary) + 10 | vMatchWidth 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/commons/JsonReads.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.commons 2 | 3 | import com.fortysevendeg.scala.android.ui.main.{UserInfo, ProjectActivityInfo} 4 | import play.api.libs.json.Json 5 | 6 | trait JsonReads { 7 | implicit val userInfoReads = Json.reads[UserInfo] 8 | implicit val projectActivityInfoReads = Json.reads[ProjectActivityInfo] 9 | } 10 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/commons/ScalaExtraTweaks.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 47 Degrees, LLC http://47deg.com hello@47deg.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | * not use this file except in compliance with the License. You may obtain 6 | * a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.fortysevendeg.scala.android.ui.commons 18 | 19 | import android.widget.ImageView 20 | import com.fortysevendeg.macroid.extras.DeviceVersion._ 21 | import com.fortysevendeg.macroid.extras.ViewTweaks._ 22 | import com.fortysevendeg.scala.android.ui.components.CircularTransformation 23 | import com.squareup.picasso.Picasso 24 | import macroid.{ActivityContextWrapper, Tweak} 25 | 26 | import scala.language.postfixOps 27 | 28 | object AsyncImageTweaks { 29 | type W = ImageView 30 | 31 | def roundedImage(url: String, 32 | placeHolder: Int, 33 | size: Int)(implicit context: ActivityContextWrapper) = CurrentVersion match { 34 | case sdk if sdk >= Lollipop => 35 | srcImage(url, placeHolder) + vCircleOutlineProvider(0) 36 | case _ => 37 | roundedImageTweak(url, placeHolder, size) 38 | } 39 | 40 | private def roundedImageTweak( 41 | url: String, 42 | placeHolder: Int, 43 | size: Int 44 | )(implicit context: ActivityContextWrapper): Tweak[W] = Tweak[W]( 45 | imageView => { 46 | Picasso.`with`(context.getOriginal) 47 | .load(url) 48 | .transform(new CircularTransformation(size)) 49 | .placeholder(placeHolder) 50 | .into(imageView) 51 | } 52 | ) 53 | 54 | def srcImage( 55 | url: String, 56 | placeHolder: Int 57 | )(implicit context: ActivityContextWrapper): Tweak[W] = Tweak[W]( 58 | imageView => { 59 | Picasso.`with`(context.getOriginal) 60 | .load(url) 61 | .placeholder(placeHolder) 62 | .into(imageView) 63 | } 64 | ) 65 | 66 | def srcImage(url: String)(implicit context: ActivityContextWrapper): Tweak[W] = Tweak[W]( 67 | imageView => { 68 | Picasso.`with`(context.getOriginal) 69 | .load(url) 70 | .into(imageView) 71 | } 72 | ) 73 | } 74 | 75 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/commons/ToolbarLayout.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.commons 2 | 3 | import android.support.v7.widget.Toolbar 4 | import android.view.ContextThemeWrapper 5 | import com.fortysevendeg.scala.android.R 6 | import com.fortysevendeg.scala.android.ui.commons.CommonsStyles._ 7 | import macroid.FullDsl._ 8 | import macroid.{ActivityContextWrapper, Ui} 9 | 10 | trait ToolbarLayout { 11 | 12 | var toolBar = slot[Toolbar] 13 | 14 | def toolBarLayout(implicit context: ActivityContextWrapper): Ui[Toolbar] = 15 | Ui { 16 | val darkToolBar = getToolbarThemeDarkActionBar 17 | toolBar = Some(darkToolBar) 18 | darkToolBar 19 | } <~ toolbarStyle 20 | 21 | private def getToolbarThemeDarkActionBar(implicit context: ActivityContextWrapper) = { 22 | val contextTheme = new ContextThemeWrapper(context.getOriginal, R.style.ThemeOverlay_AppCompat_Dark_ActionBar) 23 | val darkToolBar = new Toolbar(contextTheme) 24 | darkToolBar.setPopupTheme(R.style.ThemeOverlay_AppCompat_Light) 25 | darkToolBar 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/components/CircleView.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.components 2 | 3 | import android.content.Context 4 | import android.graphics.Paint.Style 5 | import android.graphics.{Canvas, Paint} 6 | import android.util.AttributeSet 7 | import android.view.View 8 | import macroid.Tweak 9 | 10 | class CircleView(context: Context, attr: AttributeSet, defStyleAttr: Int) 11 | extends View(context, attr, defStyleAttr) { 12 | 13 | def this(context: Context) = this(context, null, 0) 14 | 15 | def this(context: Context, attr: AttributeSet) = this(context, attr, 0) 16 | 17 | private val paint = { 18 | val paint : Paint = new Paint() 19 | paint.setStyle(Style.FILL) 20 | paint 21 | } 22 | 23 | def setColor(color: Int) = { 24 | paint.setColor(color) 25 | } 26 | 27 | override def onDraw(canvas: Canvas): Unit = { 28 | super.onDraw(canvas) 29 | canvas.drawCircle(getWidth() / 2, getHeight() / 2, getWidth() / 2, paint) 30 | } 31 | 32 | } 33 | 34 | object CircleView { 35 | type W = CircleView 36 | 37 | def cvColor(color: Int) = Tweak[W] (_.setColor(color)) 38 | } -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/components/CircularTransformation.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 47 Degrees, LLC http://47deg.com hello@47deg.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | * not use this file except in compliance with the License. You may obtain 6 | * a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.fortysevendeg.scala.android.ui.components 18 | 19 | import android.graphics.{Bitmap, Canvas, Paint, PorterDuff, PorterDuffXfermode, Rect} 20 | import com.squareup.picasso.Transformation 21 | 22 | class CircularTransformation(size: Int) extends Transformation { 23 | 24 | val radius = Math.ceil(size / 2).toInt 25 | 26 | def transform(source: Bitmap): Bitmap = { 27 | val output: Bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888) 28 | val canvas: Canvas = new Canvas(output) 29 | val color: Int = 0xff424242 30 | val paint: Paint = new Paint 31 | val rect: Rect = new Rect(0, 0, source.getWidth, source.getHeight) 32 | val target: Rect = new Rect(0, 0, size, size) 33 | paint.setAntiAlias(true) 34 | canvas.drawARGB(0, 0, 0, 0) 35 | paint.setColor(color) 36 | canvas.drawCircle(radius, radius, radius, paint) 37 | paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)) 38 | canvas.drawBitmap(source, rect, target, paint) 39 | source.recycle() 40 | output 41 | } 42 | 43 | def key: String = { 44 | s"radius-$size" 45 | } 46 | } -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/components/PathMorphDrawable.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.components 2 | 3 | import android.animation.ValueAnimator.AnimatorUpdateListener 4 | import android.animation.{Animator, AnimatorListenerAdapter, ValueAnimator} 5 | import android.graphics.Paint.Style 6 | import android.graphics._ 7 | import android.graphics.drawable.{Animatable, Drawable} 8 | import android.view.animation.DecelerateInterpolator 9 | import android.widget.ImageView 10 | import macroid.{ContextWrapper, Tweak} 11 | import IconTypes._ 12 | 13 | import scala.util.Try 14 | 15 | class PathMorphDrawable(val defaultIcon: Int = NOICON, val defaultStroke: Int = 3, val defaultColor: Int = Color.BLACK)(implicit context: ContextWrapper) 16 | extends Drawable 17 | with Animatable 18 | with PathMorphDrawableTypes { 19 | 20 | implicit var size: Option[Dim] = None 21 | 22 | lazy val burgerIcon = List( 23 | new Segment().fromRatios(0.2f, 0.3f, 0.8f, 0.3f), 24 | new Segment().fromRatios(0.2f, 0.5f, 0.8f, 0.5f), 25 | new Segment().fromRatios(0.2f, 0.7f, 0.8f, 0.7f) 26 | ) 27 | 28 | lazy val backIcon = List( 29 | new Segment().fromRatios(0.3f, 0.51f, 0.5f, 0.3f), 30 | new Segment().fromRatios(0.33f, 0.5f, 0.7f, 0.5f), 31 | new Segment().fromRatios(0.3f, 0.49f, 0.5f, 0.7f) 32 | ) 33 | 34 | lazy val upIcon = List( 35 | new Segment().fromRatios(0.49f, 0.3f, 0.7f, 0.5f), 36 | new Segment().fromRatios(0.5f, 0.33f, 0.5f, 0.7f), 37 | new Segment().fromRatios(0.51f, 0.3f, 0.3f, 0.5f) 38 | ) 39 | 40 | lazy val downIcon = List( 41 | new Segment().fromRatios(0.51f, 0.7f, 0.3f, 0.5f), 42 | new Segment().fromRatios(0.5f, 0.67f, 0.5f, 0.3f), 43 | new Segment().fromRatios(0.49f, 0.7f, 0.7f, 0.5f) 44 | ) 45 | 46 | lazy val nextIcon = List( 47 | new Segment().fromRatios(0.7f, 0.49f, 0.5f, 0.7f), 48 | new Segment().fromRatios(0.67f, 0.5f, 0.3f, 0.5f), 49 | new Segment().fromRatios(0.7f, 0.51f, 0.5f, 0.3f) 50 | ) 51 | 52 | lazy val checkIcon = List( 53 | new Segment().fromRatios(0.2f, 0.6f, 0.4f, 0.8f), 54 | new Segment().fromRatios(0.4f, 0.8f, 0.8f, 0.2f) 55 | ) 56 | 57 | lazy val addIcon = List( 58 | new Segment().fromRatios(0.5f, 0.2f, 0.5f, 0.8f), 59 | new Segment().fromRatios(0.2f, 0.5f, 0.8f, 0.5f) 60 | ) 61 | 62 | lazy val closeIcon = List( 63 | new Segment().fromRatios(0.712f, 0.288f, 0.288f, 0.712f), 64 | new Segment().fromRatios(0.288f, 0.288f, 0.712f, 0.712f) 65 | ) 66 | 67 | val noIcon = List.empty 68 | 69 | val iconPaint: Paint = { 70 | val paint = new Paint 71 | paint.setAntiAlias(true) 72 | paint.setStyle(Style.STROKE) 73 | paint.setStrokeWidth(defaultStroke) 74 | paint.setColor(defaultColor) 75 | paint 76 | } 77 | 78 | var running = false 79 | 80 | var currentIcon: Option[Icon] = None 81 | 82 | var toIcon: Option[Icon] = None 83 | 84 | var transformIcon: Option[Icon] = None 85 | 86 | override def onBoundsChange(bounds: Rect): Unit = { 87 | super.onBoundsChange(bounds) 88 | size = Some(new Dim(bounds.width(), bounds.height())) 89 | setTypeIcon(defaultIcon) 90 | } 91 | 92 | override def draw(canvas: Canvas): Unit = { 93 | if (running) { 94 | transformIcon.map(drawIcon(canvas, _)) 95 | } else { 96 | currentIcon.map(drawIcon(canvas, _)) 97 | } 98 | } 99 | 100 | override def setColorFilter(cf: ColorFilter): Unit = iconPaint.setColorFilter(cf) 101 | 102 | override def setAlpha(alpha: Int): Unit = iconPaint.setAlpha(alpha) 103 | 104 | override def getOpacity: Int = PixelFormat.TRANSPARENT 105 | 106 | override def stop(): Unit = { 107 | toIcon map setIcon 108 | toIcon = None 109 | running = false 110 | } 111 | 112 | override def isRunning: Boolean = running 113 | 114 | override def start(): Unit = { 115 | (toIcon, currentIcon) match { 116 | case (Some(to), Some(current)) => running = true; moveIcon(current, to) 117 | case (Some(to), None) => setIcon(to); toIcon = None 118 | case _ => () 119 | } 120 | } 121 | 122 | def setColor(color: Int): Unit = { 123 | iconPaint.setColor(color) 124 | invalidateSelf() 125 | } 126 | 127 | def setColorResource(color: Int): Unit = { 128 | iconPaint.setColor(context.application.getResources.getColor(color)) 129 | invalidateSelf() 130 | } 131 | 132 | def setStroke(stroke: Float) = { 133 | iconPaint.setStrokeWidth(stroke) 134 | invalidateSelf() 135 | } 136 | 137 | def setTransformIcon(icon: Icon) = { 138 | transformIcon = Some(icon) 139 | invalidateSelf() 140 | } 141 | 142 | def setIcon(icon: Icon) = { 143 | currentIcon = Some(icon) 144 | invalidateSelf() 145 | } 146 | 147 | def setToIcon(icon: Icon) = { 148 | toIcon = Some(icon) 149 | } 150 | 151 | def setTypeIcon(icon: Int) = icon match { 152 | case ADD => setIcon(addIcon) 153 | case BACK => setIcon(backIcon) 154 | case BURGER => setIcon(burgerIcon) 155 | case CHECK => setIcon(checkIcon) 156 | case CLOSE => setIcon(closeIcon) 157 | case DOWN => setIcon(downIcon) 158 | case NEXT => setIcon(nextIcon) 159 | case NOICON => setIcon(noIcon) 160 | case UP => setIcon(upIcon) 161 | } 162 | 163 | def setToTypeIcon(icon: Int) = icon match { 164 | case ADD => setToIcon(addIcon) 165 | case BACK => setToIcon(backIcon) 166 | case BURGER => setToIcon(burgerIcon) 167 | case CHECK => setToIcon(checkIcon) 168 | case CLOSE => setToIcon(closeIcon) 169 | case DOWN => setToIcon(downIcon) 170 | case NEXT => setToIcon(nextIcon) 171 | case NOICON => setToIcon(noIcon) 172 | case UP => setToIcon(upIcon) 173 | } 174 | 175 | private def drawIcon(canvas: Canvas, icon: Icon): Unit = icon foreach (drawSegment(canvas, _)) 176 | 177 | private def drawSegment(canvas: Canvas, segment: Segment): Unit = { 178 | iconPaint.setAlpha((segment.alpha * 255).toInt) 179 | canvas.drawLine(segment.point1.x, segment.point1.y, segment.point2.x, segment.point2.y, iconPaint) 180 | } 181 | 182 | def moveIcon(from: Icon, to: Icon) = { 183 | val valueAnimator: ValueAnimator = ValueAnimator.ofInt(0, 100) 184 | valueAnimator.addUpdateListener(new AnimatorUpdateListener { 185 | override def onAnimationUpdate(animation: ValueAnimator): Unit = { 186 | val fraction = animation.getAnimatedFraction 187 | 188 | val fromOver = from.drop(to.length) 189 | val toOver = to.drop(from.length) 190 | 191 | val transform = from.zip(to) map (i => transformSegment(i._1, i._2, fraction)) 192 | 193 | val segmentFromOver = fromOver map (_.copy(alpha = 1 - fraction)) 194 | 195 | val segmentToOver = toOver map { segment => 196 | transformSegment(new Segment( 197 | Point(segment.point1.x + 1, segment.point1.y + 1), 198 | Point(segment.point1.x, segment.point1.y)), segment, fraction) 199 | } 200 | 201 | val list = transform ++ segmentFromOver ++ segmentToOver 202 | 203 | setTransformIcon(list) 204 | 205 | } 206 | }) 207 | valueAnimator.setInterpolator(new DecelerateInterpolator()) 208 | valueAnimator.addListener(new AnimatorListenerAdapter { 209 | override def onAnimationEnd(animation: Animator): Unit = { 210 | super.onAnimationEnd(animation) 211 | stop() 212 | } 213 | }) 214 | valueAnimator.start() 215 | } 216 | 217 | def transformSegment(from: Segment, to: Segment, fraction: Float): Segment = 218 | if (from.equals(to)) { 219 | from 220 | } else { 221 | val point1 = calculatePoint(from.point1, to.point1, fraction) 222 | val point2 = calculatePoint(from.point2, to.point2, fraction) 223 | 224 | Segment(point1, point2) 225 | } 226 | 227 | def calculatePoint(from: Point, to: Point, fraction: Float): Point = { 228 | val cathetiX = to.x - from.x 229 | val cathetiY = to.y - from.y 230 | 231 | val hypotenuse = Math.sqrt((cathetiX * cathetiX) + (cathetiY * cathetiY)).toFloat 232 | val angle = Math.atan(cathetiY / cathetiX) 233 | 234 | val rFraction = hypotenuse * fraction 235 | 236 | val coordX = rFraction * Math.cos(angle).toFloat 237 | val coordY = rFraction * Math.sin(angle).toFloat 238 | 239 | if (cathetiX >= 0) 240 | Point(from.x + coordX, from.y + coordY) 241 | else 242 | Point(from.x - coordX, from.y - coordY) 243 | } 244 | } 245 | 246 | trait PathMorphDrawableTypes { 247 | type Icon = List[Segment] 248 | } 249 | 250 | object IconTypes { 251 | val NOICON = 0 252 | val BURGER = 1 253 | val BACK = 2 254 | val CHECK = 3 255 | val ADD = 4 256 | val UP = 5 257 | val DOWN = 6 258 | val NEXT = 7 259 | val CLOSE = 8 260 | } 261 | 262 | case class Dim(wight: Int, height: Int) 263 | 264 | case class Point(x: Float, y: Float) 265 | 266 | case class Segment( 267 | point1: Point = Point(0, 0), 268 | point2: Point = Point(0, 0), 269 | alpha: Float = 1) { 270 | 271 | def fromRatios(ratioX1: Float, 272 | ratioY1: Float, 273 | ratioX2: Float, 274 | ratioY2: Float)(implicit dim: Option[Dim]): Segment = { 275 | val (x1: Float, y1: Float, x2: Float, y2: Float) = dim.map { 276 | value => 277 | val x1 = ratioX1 * value.wight 278 | val y1 = ratioY1 * value.height 279 | val x2 = ratioX2 * value.wight 280 | val y2 = ratioY2 * value.height 281 | (x1, y1, x2, y2) 282 | }.getOrElse(0f, 0f, 0f, 0f, 0f) 283 | Segment(Point(x1, y1), Point(x2, y2)) 284 | } 285 | } 286 | 287 | object PathMorphDrawableTweaks { 288 | type W = ImageView 289 | 290 | def pmdAnimIcon(icon: Int) = Tweak[W] { 291 | view => 292 | view.getDrawable.asInstanceOf[PathMorphDrawable].setToTypeIcon(icon) 293 | view.getDrawable.asInstanceOf[PathMorphDrawable].start 294 | } 295 | def pmdChangeIcon(icon: Int) = Tweak[W](view => 296 | Try(view.getDrawable.asInstanceOf[PathMorphDrawable].setTypeIcon(icon))) 297 | 298 | def pmdColor(color: Int) = Tweak[W](view => 299 | Try(view.getDrawable.asInstanceOf[PathMorphDrawable].setColor(color))) 300 | 301 | def pmdColorResource(color: Int) = Tweak[W](view => 302 | Try(view.getDrawable.asInstanceOf[PathMorphDrawable].setColorResource(color))) 303 | 304 | def pmdStroke(stroke: Float) = Tweak[W](view => 305 | Try(view.getDrawable.asInstanceOf[PathMorphDrawable].setStroke(stroke))) 306 | } 307 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/components/RippleBackgroundView.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.components 2 | 3 | import android.animation.{Animator, AnimatorListenerAdapter} 4 | import android.annotation.TargetApi 5 | import android.content.Context 6 | import android.os.Build 7 | import android.util.AttributeSet 8 | import android.view.View._ 9 | import android.view.{View, ViewAnimationUtils, ViewGroup} 10 | import android.widget.FrameLayout 11 | import com.fortysevendeg.macroid.extras.SnailsUtils 12 | import com.fortysevendeg.macroid.extras.ViewTweaks._ 13 | import macroid.FullDsl._ 14 | import macroid.{ActivityContextWrapper, Snail} 15 | 16 | import scala.concurrent.Promise 17 | 18 | class RippleBackgroundView(context: Context, attr: AttributeSet, defStyleAttr: Int)(implicit acontext: ActivityContextWrapper) 19 | extends FrameLayout(context, attr, defStyleAttr) { 20 | 21 | def this(context: Context)(implicit acontext: ActivityContextWrapper) = this(context, null, 0) 22 | 23 | def this(context: Context, attr: AttributeSet)(implicit acontext: ActivityContextWrapper) = this(context, attr, 0) 24 | 25 | val rippleView: View = { 26 | val rippleView = getUi( 27 | w[View] <~ vInvisible 28 | ) 29 | addView(rippleView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)) 30 | rippleView 31 | } 32 | 33 | } 34 | 35 | object RippleBackgroundSnails { 36 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 37 | def ripple(rippleData : RippleSnailData) = Snail[RippleBackgroundView] { 38 | view ⇒ 39 | val animPromise = Promise[Unit]() 40 | view.rippleView.setVisibility(View.VISIBLE) 41 | view.rippleView.setBackgroundColor(rippleData.resColor) 42 | val anim = ViewAnimationUtils.createCircularReveal(view.rippleView, rippleData.x, rippleData.y, 0, rippleData.radius) 43 | anim.addListener(new AnimatorListenerAdapter { 44 | override def onAnimationEnd(animation: Animator) { 45 | super.onAnimationEnd(animation) 46 | view.rippleView.setVisibility(INVISIBLE) 47 | view.setBackgroundColor(rippleData.resColor) 48 | rippleData.onAnimationEnd foreach (listener => listener()) 49 | animPromise.success() 50 | } 51 | }) 52 | anim.setDuration(rippleData.duration) 53 | anim.start() 54 | animPromise.future 55 | } 56 | } 57 | 58 | 59 | case class RippleSnailData( 60 | width: Int = 0, 61 | height: Int = 0, 62 | x: Int = 0, 63 | y: Int = 0, 64 | radius: Int = 0, 65 | resColor: Int = 0, 66 | duration: Int = 300, 67 | onAnimationEnd: Option[() ⇒ Unit] = None) 68 | 69 | object RippleSnailData { 70 | 71 | def toCenterView(view : View): RippleSnailData = { 72 | 73 | val width = view.getWidth 74 | 75 | val height = view.getHeight 76 | 77 | val x = width / 2 78 | 79 | val y = height / 2 80 | 81 | val radius = SnailsUtils.calculateRadius(x, y, width, height) 82 | 83 | RippleSnailData( 84 | height = height, 85 | y = y, 86 | x = x, 87 | radius = radius) 88 | 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/googlemaps/CustomMapFragment.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.googlemaps 2 | 3 | import android.os.Bundle 4 | import com.fortysevendeg.scala.android.R 5 | import com.google.android.gms.maps.model.{MarkerOptions, LatLng} 6 | import com.google.android.gms.maps.{CameraUpdateFactory, GoogleMap, OnMapReadyCallback, SupportMapFragment} 7 | import googlemaps._ 8 | 9 | trait CustomMapFragment 10 | extends SupportMapFragment { 11 | 12 | def changeMapType(mapType: Int): Unit 13 | def moveCamera(latitude: Double, longitude: Double, zoom: Int): Unit 14 | def addMarker(title: String, snippet: String, latitude: Double, longitude: Double): Unit 15 | def clearMap(): Unit 16 | } 17 | 18 | class CustomMapFragmentImpl extends CustomMapFragment with OnMapReadyCallback { 19 | 20 | var googleMap: Option[GoogleMap] = None 21 | 22 | override def onCreate(savedInstanceState: Bundle) = { 23 | super.onCreate(savedInstanceState) 24 | getMapAsync(this) 25 | } 26 | 27 | override def changeMapType(mapType: Int) = { 28 | googleMap map (_.setMapType(mapType)) 29 | } 30 | 31 | override def moveCamera(latitude: Double, longitude: Double, zoom: Int = defaultZoom) = { 32 | googleMap map (_.moveCamera( 33 | CameraUpdateFactory.newLatLngZoom(new LatLng(latitude, longitude), zoom))) 34 | } 35 | 36 | override def addMarker(title: String, snippet: String, latitude: Double, longitude: Double) = { 37 | googleMap map { 38 | val marker = new MarkerOptions() 39 | .title(title) 40 | .snippet(snippet) 41 | .position(new LatLng(latitude, longitude)) 42 | _.addMarker(marker) 43 | } 44 | } 45 | 46 | override def clearMap() = { 47 | googleMap map (_.clear()) 48 | } 49 | 50 | override def onMapReady(googleMap: GoogleMap) = { 51 | this.googleMap = Some(googleMap) 52 | googleMap.setMyLocationEnabled(true) 53 | moveCamera(initLatitude, initLongitude) 54 | addMarker( 55 | getString(R.string.marker_map_title), 56 | getString(R.string.marker_map_message), 57 | initLatitude, 58 | initLongitude 59 | ) 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/googlemaps/GoogleMapsActivity.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.googlemaps 2 | 3 | import android.os.Bundle 4 | import android.support.v4.app.FragmentActivity 5 | import android.support.v7.app.AppCompatActivity 6 | import android.view.{Menu, MenuItem} 7 | import com.fortysevendeg.macroid.extras.FragmentExtras._ 8 | import com.fortysevendeg.scala.android.R 9 | import macroid.Contexts 10 | import googlemaps._ 11 | 12 | import scala.util.Random 13 | 14 | class GoogleMapsActivity 15 | extends AppCompatActivity 16 | with Contexts[FragmentActivity] 17 | with Layout { 18 | 19 | val random = new Random() 20 | 21 | override def onCreate(savedInstanceState: Bundle) = { 22 | super.onCreate(savedInstanceState) 23 | 24 | setContentView(layout) 25 | 26 | toolBar map setSupportActionBar 27 | 28 | getSupportActionBar().setDisplayHomeAsUpEnabled(true) 29 | 30 | } 31 | 32 | override def onCreateOptionsMenu(menu: Menu): Boolean = { 33 | getMenuInflater.inflate(R.menu.activity_google_maps, menu) 34 | true 35 | } 36 | 37 | override def onOptionsItemSelected(item: MenuItem): Boolean = { 38 | item.getItemId match { 39 | case android.R.id.home => { 40 | finish() 41 | false 42 | } 43 | case R.id.add_marker => { 44 | findFragmentByTag[CustomMapFragment](fragmentTag) map (_.addMarker( 45 | getString(R.string.marker_map_sample_title), 46 | getString(R.string.marker_map_sample_message), 47 | initLatitude + random.nextDouble() / 100, 48 | initLongitude + random.nextDouble() / 100)) 49 | true 50 | } 51 | case R.id.clear_map => { 52 | findFragmentByTag[CustomMapFragment](fragmentTag) map (_.clearMap()) 53 | true 54 | } 55 | } 56 | super.onOptionsItemSelected(item) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/googlemaps/Layout.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.googlemaps 2 | 3 | import android.support.v4.app.{FragmentManager, Fragment} 4 | import android.widget.{Button, LinearLayout} 5 | import com.fortysevendeg.scala.android.R 6 | import com.fortysevendeg.scala.android.ui.commons.ToolbarLayout 7 | import com.fortysevendeg.macroid.extras.ToolbarTweaks._ 8 | import com.fortysevendeg.macroid.extras.TextTweaks._ 9 | import com.fortysevendeg.macroid.extras.FragmentExtras._ 10 | import com.google.android.gms.maps.GoogleMap 11 | import macroid.FullDsl._ 12 | import macroid._ 13 | 14 | trait Layout 15 | extends ToolbarLayout 16 | with IdGeneration 17 | with Styles { 18 | 19 | val fragmentTag = "fragment-map" 20 | 21 | def layout(implicit context: ActivityContextWrapper, fragmentManager: FragmentManagerContext[Fragment, FragmentManager]) = { 22 | getUi( 23 | l[LinearLayout]( 24 | toolBarLayout <~ tbTitle(R.string.title_map), 25 | l[LinearLayout]( 26 | w[Button] <~ tvText(R.string.map_satellite) <~ buttonsStyle <~ On.click { 27 | Ui { 28 | findFragmentByTag[CustomMapFragment](fragmentTag) map (_.changeMapType(GoogleMap.MAP_TYPE_SATELLITE)) 29 | } 30 | }, 31 | w[Button] <~ tvText(R.string.map_normal) <~ buttonsStyle <~ On.click { 32 | Ui { 33 | findFragmentByTag[CustomMapFragment](fragmentTag) map (_.changeMapType(GoogleMap.MAP_TYPE_NORMAL)) 34 | } 35 | }, 36 | w[Button] <~ tvText(R.string.map_hybrid) <~ buttonsStyle <~ On.click { 37 | Ui { 38 | findFragmentByTag[CustomMapFragment](fragmentTag) map (_.changeMapType(GoogleMap.MAP_TYPE_HYBRID)) 39 | } 40 | }, 41 | w[Button] <~ tvText(R.string.map_terrain) <~ buttonsStyle <~ On.click { 42 | Ui { 43 | findFragmentByTag[CustomMapFragment](fragmentTag) map (_.changeMapType(GoogleMap.MAP_TYPE_TERRAIN)) 44 | } 45 | } 46 | ) <~ horizontalLinearLayoutStyle, 47 | f[CustomMapFragmentImpl].framed(Id.map, fragmentTag) 48 | ) <~ contentStyle 49 | ) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/googlemaps/Styles.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.googlemaps 2 | 3 | import android.widget.{Button, LinearLayout} 4 | import com.fortysevendeg.macroid.extras.LinearLayoutTweaks._ 5 | import com.fortysevendeg.macroid.extras.TextTweaks._ 6 | import com.fortysevendeg.macroid.extras.ViewTweaks._ 7 | import macroid.Tweak 8 | 9 | import scala.language.postfixOps 10 | 11 | trait Styles { 12 | 13 | val contentStyle: Tweak[LinearLayout] = llVertical 14 | 15 | val horizontalLinearLayoutStyle: Tweak[LinearLayout] = 16 | vMatchWidth + 17 | llHorizontal 18 | 19 | val buttonsStyle: Tweak[Button] = llWrapWeightHorizontal + tvMaxLines(1) 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/googlemaps/googlemaps.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.googlemaps 2 | 3 | object googlemaps { 4 | 5 | val defaultZoom = 13 6 | val initLatitude = 36.462912 7 | val initLongitude = -6.199047 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/main/Layout.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.main 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import android.support.v7.widget.{CardView, RecyclerView} 6 | import android.widget.{ImageView, LinearLayout, TextView} 7 | import com.fortysevendeg.scala.android.R 8 | import com.fortysevendeg.scala.android.ui.commons.ToolbarLayout 9 | import macroid.FullDsl._ 10 | import macroid.{Ui, ActivityContextWrapper} 11 | import com.fortysevendeg.macroid.extras.TextTweaks._ 12 | import com.fortysevendeg.macroid.extras.ResourcesExtras._ 13 | 14 | trait Layout 15 | extends ToolbarLayout 16 | with Styles { 17 | 18 | var content = slot[LinearLayout] 19 | 20 | var recyclerView = slot[RecyclerView] 21 | 22 | def layout(implicit context: ActivityContextWrapper) = { 23 | getUi( 24 | l[LinearLayout]( 25 | toolBarLayout, 26 | w[RecyclerView] <~ wire(recyclerView) <~ listStyle 27 | ) <~ wire(content) <~ contentStyle 28 | ) 29 | } 30 | 31 | } 32 | 33 | class Adapter(implicit context: ActivityContextWrapper) 34 | extends AdapterStyles { 35 | 36 | var title = slot[TextView] 37 | 38 | var description = slot[TextView] 39 | 40 | var api = slot[TextView] 41 | 42 | var username = slot[TextView] 43 | 44 | var twitter = slot[TextView] 45 | 46 | var avatar = slot[ImageView] 47 | 48 | var androidLevel = slot[TextView] 49 | 50 | var scalaLevel = slot[TextView] 51 | 52 | var userContent = slot[LinearLayout] 53 | 54 | val content = { 55 | getUi( 56 | l[CardView]( 57 | l[LinearLayout]( 58 | l[LinearLayout]( 59 | w[TextView] <~ wire(title) <~ titleStyle, 60 | w[TextView] <~ wire(api) <~ apiStyle 61 | ) <~ itemTopStyle, 62 | w[TextView] <~ wire(description) <~ descriptionStyle, 63 | w[ImageView] <~ lineHorizontalStyle, 64 | l[LinearLayout]( 65 | l[LinearLayout]( 66 | w[ImageView] <~ wire(avatar) <~ avatarStyle, 67 | l[LinearLayout]( 68 | w[TextView] <~ wire(username) <~ userNameStyle, 69 | w[TextView] <~ wire(twitter) <~ twitterStyle 70 | ) <~ userNameContentStyle 71 | ) <~ bottomUserContentStyle <~ wire(userContent) <~ On.click { 72 | Ui { 73 | for { 74 | content <- userContent 75 | tag <- Option(content.getTag) 76 | } yield { 77 | val twitterName = if (tag.toString.startsWith("@")) tag.toString.substring(1) else tag.toString 78 | context.getOriginal.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(resGetString(R.string.url_twitter_user, twitterName)))) 79 | } 80 | } 81 | }, 82 | w[ImageView] <~ lineVerticalStyle, 83 | l[LinearLayout]( 84 | l[LinearLayout]( 85 | w[TextView] <~ levelStyle <~ tvText(R.string.scala_level), 86 | w[TextView] <~ levelTypeStyle <~ wire(scalaLevel) 87 | ) <~ levelItemContentStyle, 88 | l[LinearLayout]( 89 | w[TextView] <~ levelStyle <~ tvText(R.string.android_level), 90 | w[TextView] <~ levelTypeStyle <~ wire(androidLevel) 91 | ) 92 | ) <~ bottomLevelsContentStyle 93 | ) <~ bottomContentStyle 94 | ) <~ itemStyle 95 | ) <~ cardStyle 96 | ) 97 | } 98 | 99 | def layout = content 100 | 101 | } -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/main/MainActivity.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.main 2 | 3 | import android.content.ComponentName 4 | import android.os.Bundle 5 | import android.support.v7.app.AppCompatActivity 6 | import android.support.v7.widget.{GridLayoutManager, LinearLayoutManager} 7 | import com.fortysevendeg.macroid.extras.DeviceMediaQueries._ 8 | import com.fortysevendeg.macroid.extras.RecyclerViewTweaks._ 9 | import com.fortysevendeg.macroid.extras.ActionsExtras._ 10 | import com.fortysevendeg.scala.android.R 11 | import macroid.Contexts 12 | import macroid.FullDsl._ 13 | 14 | class MainActivity 15 | extends AppCompatActivity 16 | with Contexts[AppCompatActivity] 17 | with Layout { 18 | 19 | override def onCreate(savedInstanceState: Bundle) = { 20 | super.onCreate(savedInstanceState) 21 | setContentView(layout) 22 | 23 | val adapter = new ProjectActivityInfoListAdapter(new RecyclerClickListener { 24 | override def onClick(info: ProjectActivityInfo): Unit = { 25 | if (info.apiType == APIType.REQUIRED) { 26 | aShortToast(getString(R.string.min_api_not_available)) 27 | } else { 28 | aStartActivityFromComponentName(new ComponentName(getPackageName, info.className)) 29 | } 30 | } 31 | }) 32 | 33 | val layoutManager = 34 | landscapeTablet ? 35 | new GridLayoutManager(this, 3) | 36 | tablet ? 37 | new GridLayoutManager(this, 2) | new LinearLayoutManager(this) 38 | 39 | runUi(recyclerView <~ rvLayoutManager(layoutManager) <~ 40 | rvAddItemDecoration(new MainItemDecorator) <~ 41 | rvAdapter(adapter)) 42 | 43 | toolBar map setSupportActionBar 44 | 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/main/MainItemDecorator.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.main 2 | 3 | import android.graphics.Rect 4 | import android.support.v7.widget.RecyclerView 5 | import android.support.v7.widget.RecyclerView.State 6 | import android.view.View 7 | import com.fortysevendeg.macroid.extras.ResourcesExtras._ 8 | import com.fortysevendeg.scala.android.R 9 | import macroid.ContextWrapper 10 | 11 | import scala.language.postfixOps 12 | 13 | class MainItemDecorator(implicit context: ContextWrapper) 14 | extends RecyclerView.ItemDecoration { 15 | 16 | override def getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: State): Unit = { 17 | outRect.top = resGetDimensionPixelSize(R.dimen.padding_default) 18 | outRect.bottom = resGetDimensionPixelSize(R.dimen.padding_default) 19 | outRect.left = resGetDimensionPixelSize(R.dimen.padding_default) 20 | outRect.right = resGetDimensionPixelSize(R.dimen.padding_default) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/main/ProjectActivityInfoListAdapter.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.main 2 | 3 | import java.io.FileNotFoundException 4 | 5 | import android.support.v7.widget.RecyclerView 6 | import android.view.View.OnClickListener 7 | import android.view.{View, ViewGroup} 8 | import com.fortysevendeg.macroid.extras.ActionsExtras._ 9 | import com.fortysevendeg.macroid.extras.DeviceVersion._ 10 | import com.fortysevendeg.macroid.extras.ResourcesExtras._ 11 | import com.fortysevendeg.macroid.extras.TextTweaks._ 12 | import com.fortysevendeg.macroid.extras.ViewTweaks._ 13 | import com.fortysevendeg.scala.android.R 14 | import com.fortysevendeg.scala.android.ui.commons.JsonReads 15 | import macroid.FullDsl._ 16 | import macroid.ActivityContextWrapper 17 | import com.fortysevendeg.scala.android.ui.commons.AsyncImageTweaks._ 18 | import play.api.libs.json.{JsResultException, Json} 19 | 20 | import scala.io.Source 21 | import scala.util.{Failure, Success, Try} 22 | 23 | class ProjectActivityInfoListAdapter(listener: RecyclerClickListener) 24 | (implicit context: ActivityContextWrapper) 25 | extends RecyclerView.Adapter[ViewHolder] with JsonReads { 26 | 27 | import com.fortysevendeg.scala.android.ui.main.APIType._ 28 | 29 | val recyclerClickListener = listener 30 | 31 | val activityInfoList: Seq[ProjectActivityInfo] = getActivitiesFromJson("activities.json") match { 32 | case Success(activitiesList) => activitiesList 33 | case Failure(exception) => { 34 | exception match { 35 | case e: FileNotFoundException => aLongToast(context.application.getString(R.string.json_file_not_found)) 36 | case e: JsResultException => aLongToast(context.application.getString(R.string.malformed_json_file)) 37 | case e => aLongToast(context.application.getString(R.string.unexpected_error) + e.toString) 38 | } 39 | Seq.empty 40 | } 41 | } 42 | 43 | override def onCreateViewHolder(parentViewGroup: ViewGroup, i: Int): ViewHolder = { 44 | val adapter = new Adapter 45 | adapter.layout.setOnClickListener(new OnClickListener { 46 | override def onClick(v: View): Unit = recyclerClickListener.onClick(activityInfoList(v.getTag.asInstanceOf[Int])) 47 | }) 48 | 49 | ViewHolder(adapter) 50 | } 51 | 52 | override def getItemCount: Int = activityInfoList.size 53 | 54 | override def onBindViewHolder(viewHolder: ViewHolder, position: Int): Unit = { 55 | val projectActivityInfo = activityInfoList(position) 56 | val resourceStringFormatArgs = Seq(projectActivityInfo.minApi) map (_.asInstanceOf[Object]) 57 | viewHolder.content.setTag(position) 58 | 59 | val avatarSize = resGetDimensionPixelSize(R.dimen.main_list_avatar_size) 60 | 61 | runUi( 62 | (viewHolder.title <~ tvText(projectActivityInfo.name)) ~ 63 | (viewHolder.userContent <~ vTag(projectActivityInfo.user.twitter)) ~ 64 | (viewHolder.description <~ tvText(projectActivityInfo.description)) ~ 65 | (viewHolder.avatar <~ roundedImage(projectActivityInfo.user.avatar, R.drawable.placeholder_circle, avatarSize)) ~ 66 | (viewHolder.username <~ tvText(projectActivityInfo.user.name)) ~ 67 | (viewHolder.twitter <~ tvText(projectActivityInfo.user.twitter)) ~ 68 | (viewHolder.api <~ tvText(resGetString(R.string.min_api_required, projectActivityInfo.minApi.toString)) <~ 69 | (projectActivityInfo.apiType match { 70 | case SUCCESS => vBackground(R.drawable.background_item_api_success) 71 | case ADVISED => vBackground(R.drawable.background_item_api_advised) 72 | case _ => vBackground(R.drawable.background_item_api_required) 73 | })) ~ 74 | (viewHolder.androidLevel <~ (projectActivityInfo.androidLevel match { 75 | case 1 => tvText(R.string.beginning_level) 76 | case 2 => tvText(R.string.intermediate_level) 77 | case 3 => tvText(R.string.advanced_level) 78 | })) ~ 79 | (viewHolder.scalaLevel <~ (projectActivityInfo.scalaLevel match { 80 | case 1 => tvText(R.string.beginning_level) 81 | case 2 => tvText(R.string.intermediate_level) 82 | case 3 => tvText(R.string.advanced_level) 83 | })) 84 | ) 85 | } 86 | 87 | private def getActivitiesFromJson(url: String): Try[Seq[ProjectActivityInfo]] = { 88 | for { 89 | stream <- Try(context.application.getAssets.open(url)) 90 | json <- Try(Source.fromInputStream(stream, "UTF-8").mkString) 91 | activityInfoList <- Try(Json.parse(json).as[Seq[ProjectActivityInfo]]) 92 | } yield activityInfoList 93 | } 94 | 95 | } 96 | 97 | trait RecyclerClickListener { 98 | def onClick(info: ProjectActivityInfo) 99 | } 100 | 101 | case class ViewHolder(adapter: Adapter)(implicit context: ActivityContextWrapper) 102 | extends RecyclerView.ViewHolder(adapter.layout) { 103 | 104 | val content = adapter.layout 105 | 106 | val title = adapter.title 107 | 108 | val description = adapter.description 109 | 110 | val api = adapter.api 111 | 112 | val username = adapter.username 113 | 114 | val twitter = adapter.twitter 115 | 116 | val avatar = adapter.avatar 117 | 118 | val scalaLevel = adapter.scalaLevel 119 | 120 | val androidLevel = adapter.androidLevel 121 | 122 | val userContent = adapter.userContent 123 | 124 | } 125 | 126 | case class ProjectActivityInfo( 127 | name: String, 128 | description: String, 129 | className: String, 130 | minApi: Int, 131 | targetApi: Int, 132 | scalaLevel: Int, 133 | androidLevel: Int, 134 | user: UserInfo) { 135 | 136 | import com.fortysevendeg.scala.android.ui.main.APIType._ 137 | 138 | val apiType: APIType.Value = CurrentVersion match { 139 | case current if current.version >= targetApi => SUCCESS 140 | case current if current.version >= minApi => ADVISED 141 | case _ => REQUIRED 142 | } 143 | } 144 | 145 | case class UserInfo( 146 | avatar: String, 147 | name: String, 148 | twitter: String) 149 | 150 | object APIType extends Enumeration { 151 | 152 | type APIType = Value 153 | 154 | val SUCCESS, ADVISED, REQUIRED = Value 155 | } -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/main/Styles.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.main 2 | 3 | import android.support.v7.widget.{CardView, RecyclerView} 4 | import android.text.TextUtils.TruncateAt 5 | import android.view.Gravity 6 | import android.view.ViewGroup.LayoutParams._ 7 | import android.widget.ImageView.ScaleType 8 | import android.widget.{ImageView, LinearLayout, TextView} 9 | import com.fortysevendeg.macroid.extras.FrameLayoutTweaks._ 10 | import com.fortysevendeg.macroid.extras.ImageViewTweaks._ 11 | import com.fortysevendeg.macroid.extras.LinearLayoutTweaks._ 12 | import com.fortysevendeg.macroid.extras.ResourcesExtras._ 13 | import com.fortysevendeg.macroid.extras.TextTweaks._ 14 | import com.fortysevendeg.macroid.extras.ThemeExtras._ 15 | import com.fortysevendeg.macroid.extras.ViewGroupTweaks._ 16 | import com.fortysevendeg.macroid.extras.ViewTweaks._ 17 | import com.fortysevendeg.scala.android.R 18 | import macroid.FullDsl._ 19 | import macroid.{ActivityContextWrapper, ContextWrapper, Tweak} 20 | 21 | import scala.language.postfixOps 22 | 23 | trait Styles { 24 | 25 | def listStyle(implicit context: ContextWrapper): Tweak[RecyclerView] = 26 | llMatchWeightVertical + 27 | vPaddings(resGetDimensionPixelSize(R.dimen.padding_default)) + 28 | vgClipToPadding(false) 29 | 30 | def contentStyle(implicit context: ContextWrapper): Tweak[LinearLayout] = 31 | llVertical + 32 | vBackgroundColorResource(R.color.main_list_background) 33 | 34 | } 35 | 36 | trait AdapterStyles { 37 | 38 | def cardStyle(implicit activityContext: ActivityContextWrapper): Tweak[CardView] = 39 | vMatchWidth + 40 | (themeGetDrawable(android.R.attr.selectableItemBackground) map flForeground getOrElse Tweak.blank) 41 | 42 | def itemStyle: Tweak[LinearLayout] = 43 | llVertical + 44 | vMatchWidth 45 | 46 | def itemTopStyle(implicit context: ContextWrapper): Tweak[LinearLayout] = 47 | llHorizontal + 48 | vMatchWidth + 49 | llGravity(Gravity.CENTER_VERTICAL) + 50 | vPadding( 51 | paddingTop = resGetDimensionPixelSize(R.dimen.padding_default_xlarge), 52 | paddingBottom = resGetDimensionPixelSize(R.dimen.padding_default), 53 | paddingLeft = resGetDimensionPixelSize(R.dimen.padding_default_xlarge), 54 | paddingRight = resGetDimensionPixelSize(R.dimen.padding_default_xlarge)) 55 | 56 | def titleStyle(implicit context: ContextWrapper): Tweak[TextView] = 57 | llWrapWeightHorizontal + 58 | tvSizeResource(R.dimen.font_size_medium) + 59 | tvColorResource(R.color.primary) 60 | 61 | def descriptionStyle(implicit context: ContextWrapper): Tweak[TextView] = 62 | tvSizeResource(R.dimen.font_size_normal) + 63 | tvNormalLight + 64 | tvColorResource(R.color.main_list_description) + 65 | tvMaxLines(3) + 66 | vPadding( 67 | paddingBottom = resGetDimensionPixelSize(R.dimen.padding_default_xlarge), 68 | paddingLeft = resGetDimensionPixelSize(R.dimen.padding_default_xlarge), 69 | paddingRight = resGetDimensionPixelSize(R.dimen.padding_default_xlarge)) 70 | 71 | def apiStyle(implicit context: ContextWrapper): Tweak[TextView] = 72 | tvSizeResource(R.dimen.font_size_micro) + 73 | tvColorResource(R.color.main_list_api) + 74 | vPaddings( 75 | paddingTopBottom = resGetDimensionPixelSize(R.dimen.padding_default_micro), 76 | paddingLeftRight = resGetDimensionPixelSize(R.dimen.padding_default_small)) 77 | 78 | def lineHorizontalStyle(implicit context: ContextWrapper): Tweak[ImageView] = 79 | lp[LinearLayout](MATCH_PARENT, resGetDimensionPixelSize(R.dimen.line)) + 80 | vBackgroundColorResource(R.color.main_list_line) 81 | 82 | val bottomContentStyle: Tweak[LinearLayout] = 83 | vMatchWidth + 84 | llHorizontal + 85 | llGravity(Gravity.CENTER_VERTICAL) 86 | 87 | def bottomUserContentStyle(implicit activityContext: ActivityContextWrapper): Tweak[LinearLayout] = 88 | llMatchWeightHorizontal + 89 | llHorizontal + 90 | vPaddings(resGetDimensionPixelSize(R.dimen.padding_default)) + 91 | llGravity(Gravity.CENTER_VERTICAL) + 92 | (themeGetDrawable(android.R.attr.selectableItemBackground) map vBackground getOrElse Tweak.blank) 93 | 94 | def avatarStyle(implicit context: ContextWrapper): Tweak[ImageView] = { 95 | val size = resGetDimensionPixelSize(R.dimen.main_list_avatar_size) 96 | lp[LinearLayout](size, size) + 97 | ivScaleType(ScaleType.CENTER_CROP) + 98 | vMargins(resGetDimensionPixelSize(R.dimen.padding_default_small)) 99 | } 100 | 101 | def userNameStyle(implicit context: ContextWrapper): Tweak[TextView] = 102 | tvSizeResource(R.dimen.font_size_normal) + 103 | tvNormalLight + 104 | tvColorResource(R.color.primary) + 105 | tvMaxLines(1) + 106 | tvEllipsize(TruncateAt.END) 107 | 108 | def twitterStyle(implicit context: ContextWrapper): Tweak[TextView] = 109 | tvSizeResource(R.dimen.font_size_small) + 110 | tvNormalLight + 111 | tvColorResource(R.color.main_list_secondary) + 112 | tvMaxLines(1) + 113 | tvEllipsize(TruncateAt.END) 114 | 115 | def userNameContentStyle(implicit context: ContextWrapper): Tweak[LinearLayout] = 116 | llMatchWeightHorizontal + 117 | llVertical + 118 | vPadding(paddingLeft = resGetDimensionPixelSize(R.dimen.padding_default_small)) + 119 | llGravity(Gravity.CENTER_VERTICAL) 120 | 121 | def lineVerticalStyle(implicit context: ContextWrapper): Tweak[ImageView] = 122 | lp[LinearLayout](resGetDimensionPixelSize(R.dimen.line), MATCH_PARENT) + 123 | vBackgroundColorResource(R.color.main_list_line) 124 | 125 | def bottomLevelsContentStyle(implicit context: ContextWrapper): Tweak[LinearLayout] = 126 | llMatchWeightHorizontal + 127 | llVertical + 128 | vPaddings(resGetDimensionPixelSize(R.dimen.padding_default)) + 129 | llGravity(Gravity.CENTER_VERTICAL) 130 | 131 | def levelItemContentStyle(implicit context: ContextWrapper): Tweak[LinearLayout] = 132 | vWrapContent + 133 | vPadding(paddingBottom = resGetDimensionPixelSize(R.dimen.padding_default_micro)) 134 | 135 | def levelStyle(implicit context: ContextWrapper): Tweak[TextView] = 136 | tvSizeResource(R.dimen.font_size_small) + 137 | tvNormalLight + 138 | tvColorResource(R.color.main_list_secondary) + 139 | vMinWidth(resGetDimensionPixelSize(R.dimen.main_list_min_width_levels_tag)) 140 | 141 | def levelTypeStyle(implicit context: ContextWrapper): Tweak[TextView] = 142 | tvSizeResource(R.dimen.font_size_small) + 143 | tvColorResource(R.color.main_list_tag) + 144 | tvNormalLight + 145 | vPaddings( 146 | paddingTopBottom = 0, 147 | paddingLeftRight = resGetDimensionPixelSize(R.dimen.padding_default_small)) + 148 | tvMaxLines(1) + 149 | tvEllipsize(TruncateAt.END) 150 | 151 | } 152 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/materiallist/Composers.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.materiallist 2 | 3 | import android.graphics.Color 4 | import android.support.design.widget.Snackbar 5 | import android.support.v7.widget.{GridLayoutManager, RecyclerView} 6 | import android.view.View 7 | import com.fortysevendeg.macroid.extras.ImageViewTweaks._ 8 | import com.fortysevendeg.macroid.extras.RecyclerViewTweaks._ 9 | import com.fortysevendeg.macroid.extras.ResourcesExtras._ 10 | import com.fortysevendeg.macroid.extras.TextTweaks._ 11 | import com.fortysevendeg.macroid.extras.ViewTweaks._ 12 | import com.fortysevendeg.scala.android.ui.commons.AsyncImageTweaks._ 13 | import com.fortysevendeg.scala.android.ui.components.IconTypes._ 14 | import com.fortysevendeg.scala.android.ui.components.PathMorphDrawable 15 | import com.fortysevendeg.scala.android.{R, TR, TypedFindView} 16 | import macroid.FullDsl._ 17 | import macroid.{ActivityContextWrapper, ContextWrapper, Ui} 18 | 19 | import scala.language.postfixOps 20 | 21 | trait MainComposer { 22 | 23 | self: TypedFindView => 24 | 25 | lazy val content = Option(findView(TR.content)) 26 | 27 | lazy val toolBar = Option(findView(TR.toolbar)) 28 | 29 | lazy val appBarLayout = Option(findView(TR.app_bar_layout)) 30 | 31 | lazy val recycler = Option(findView(TR.recycler)) 32 | 33 | lazy val fabActionButton = Option(findView(TR.fab_action_button)) 34 | 35 | def composition(implicit contextWrapper: ActivityContextWrapper): Ui[_] = initRecycler ~ initFabButton 36 | 37 | private[this] def initRecycler(implicit contextWrapper: ActivityContextWrapper): Ui[_] = 38 | (recycler 39 | <~ rvAdapter(new ImageListAdapter()) 40 | <~ rvFixedSize 41 | <~ rvLayoutManager(new GridLayoutManager(contextWrapper.bestAvailable, 2))) 42 | 43 | private[this] def initFabButton(implicit contextWrapper: ContextWrapper): Ui[_] = 44 | (fabActionButton 45 | <~ ivSrc(fabDrawable) 46 | <~ On.click { 47 | Ui { 48 | content foreach (Snackbar.make(_, resGetString(R.string.material_list_add_item), Snackbar.LENGTH_LONG).show()) 49 | } 50 | }) 51 | 52 | private[this] def fabDrawable(implicit contextWrapper: ContextWrapper) = new PathMorphDrawable( 53 | defaultIcon = ADD, 54 | defaultStroke = resGetDimensionPixelSize(R.dimen.circular_reveal_fab_stroke), 55 | defaultColor = Color.WHITE 56 | ) 57 | 58 | } 59 | 60 | case class ImageViewHolder(parent: View) 61 | extends RecyclerView.ViewHolder(parent) 62 | with TypedFindView { 63 | 64 | lazy val image = Option(findView(TR.image)) 65 | lazy val text = Option(findView(TR.text)) 66 | 67 | override protected def findViewById(id: Int): View = parent.findViewById(id) 68 | 69 | def bind(imageData: ImageData, position: Int)(implicit contextWrapper: ActivityContextWrapper): Ui[_] = 70 | (parent <~ vTag(position.toString)) ~ 71 | (image <~ srcImage(imageData.url)) ~ 72 | (text <~ tvText(imageData.name)) 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/materiallist/FABAnimationBehavior.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.materiallist 2 | 3 | import android.animation.{Animator, AnimatorListenerAdapter} 4 | import android.content.Context 5 | import android.support.design.widget.{CoordinatorLayout, FloatingActionButton} 6 | import android.support.v4.view.ViewCompat 7 | import android.support.v4.view.animation.FastOutSlowInInterpolator 8 | import android.util.AttributeSet 9 | import android.view.View 10 | import macroid.Snail 11 | import macroid.FullDsl._ 12 | import scala.concurrent.ExecutionContext.Implicits.global 13 | 14 | import scala.concurrent.Promise 15 | 16 | class FABAnimationBehavior 17 | extends FloatingActionButton.Behavior { 18 | 19 | def this(context: Context, attrs: AttributeSet) = this() 20 | 21 | var isAnimatingOut = false 22 | 23 | val interpolator = new FastOutSlowInInterpolator() 24 | 25 | val duration = 200L 26 | 27 | override def onStartNestedScroll( 28 | coordinatorLayout: CoordinatorLayout, 29 | child: FloatingActionButton, 30 | directTargetChild: View, 31 | target: View, 32 | nestedScrollAxes: Int): Boolean = 33 | nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL || 34 | super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes) 35 | 36 | override def onNestedScroll( 37 | coordinatorLayout: CoordinatorLayout, 38 | child: FloatingActionButton, 39 | target: View, 40 | dxConsumed: Int, 41 | dyConsumed: Int, 42 | dxUnconsumed: Int, 43 | dyUnconsumed: Int): Unit = { 44 | super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed) 45 | 46 | (dyConsumed, child) match { 47 | case (d, c) if d > 0 && !isAnimatingOut && c.getVisibility == View.VISIBLE => 48 | runUi(Option(child) <~~ animateOut) 49 | case (d, c) if d < 0 && c.getVisibility != View.VISIBLE => 50 | runUi(Option(child) <~~ animateIn) 51 | case _ => 52 | } 53 | } 54 | 55 | val animateIn = Snail[FloatingActionButton] { 56 | view ⇒ 57 | view.setVisibility(View.VISIBLE) 58 | val animPromise = Promise[Unit]() 59 | view.animate 60 | .translationY(0) 61 | .setInterpolator(interpolator) 62 | .setDuration(duration) 63 | .setListener(new AnimatorListenerAdapter { 64 | override def onAnimationEnd(animation: Animator) { 65 | super.onAnimationEnd(animation) 66 | animPromise.success() 67 | } 68 | }).start() 69 | animPromise.future 70 | } 71 | 72 | val animateOut = Snail[FloatingActionButton] { 73 | view ⇒ 74 | val animPromise = Promise[Unit]() 75 | val y = view.getHeight + (view.getPaddingBottom * 2) 76 | view.animate 77 | .translationY(y) 78 | .setInterpolator(interpolator) 79 | .setDuration(duration) 80 | .setListener(new AnimatorListenerAdapter { 81 | override def onAnimationStart(animation: Animator): Unit = { 82 | super.onAnimationStart(animation) 83 | isAnimatingOut = true 84 | } 85 | override def onAnimationCancel(animation: Animator): Unit = { 86 | super.onAnimationCancel(animation) 87 | isAnimatingOut = false 88 | } 89 | override def onAnimationEnd(animation: Animator) { 90 | super.onAnimationEnd(animation) 91 | isAnimatingOut = false 92 | view.setVisibility(View.GONE) 93 | animPromise.success() 94 | } 95 | }).start() 96 | animPromise.future 97 | } 98 | } -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/materiallist/ImageListAdapter.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.materiallist 2 | 3 | import android.support.design.widget.Snackbar 4 | import android.support.v7.widget.RecyclerView 5 | import android.view.{LayoutInflater, View, ViewGroup} 6 | import com.fortysevendeg.scala.android.R 7 | import macroid.ActivityContextWrapper 8 | import macroid.FullDsl._ 9 | 10 | class ImageListAdapter(implicit context: ActivityContextWrapper) 11 | extends RecyclerView.Adapter[ImageViewHolder] 12 | with View.OnClickListener { 13 | 14 | val images: Seq[ImageData] = 1 to 10 map { 15 | i => 16 | ImageData(s"Item $i", s"http://lorempixel.com/500/500/animals/$i") 17 | } 18 | 19 | override def onCreateViewHolder(parent: ViewGroup, i: Int): ImageViewHolder = { 20 | val v = LayoutInflater.from(parent.getContext).inflate(R.layout.image_item, parent, false) 21 | v.setOnClickListener(this) 22 | new ImageViewHolder(v) 23 | } 24 | 25 | override def getItemCount: Int = images.size 26 | 27 | override def onBindViewHolder(viewHolder: ImageViewHolder, position: Int): Unit = 28 | runUi(viewHolder.bind(images(position), position)) 29 | 30 | override def onClick(v: View): Unit = { 31 | val image = images(v.getTag.toString.toInt) 32 | Snackbar.make(v, image.name, Snackbar.LENGTH_LONG).show() 33 | } 34 | } 35 | 36 | case class ImageData(name: String, url: String) 37 | 38 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/materiallist/MaterialListActivity.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.materiallist 2 | 3 | import android.os.Bundle 4 | import android.support.v7.app.AppCompatActivity 5 | import android.view.MenuItem 6 | import com.fortysevendeg.scala.android.{R, TypedFindView} 7 | import macroid.Contexts 8 | import macroid.FullDsl._ 9 | 10 | class MaterialListActivity 11 | extends AppCompatActivity 12 | with TypedFindView 13 | with MainComposer 14 | with Contexts[AppCompatActivity] { 15 | 16 | override def onCreate(savedInstanceState: Bundle) = { 17 | super.onCreate(savedInstanceState) 18 | 19 | setContentView(R.layout.material_list_activity) 20 | 21 | toolBar foreach setSupportActionBar 22 | getSupportActionBar.setDisplayHomeAsUpEnabled(true) 23 | 24 | runUi(composition) 25 | } 26 | 27 | override def onOptionsItemSelected(item: MenuItem): Boolean = item.getItemId match { 28 | case android.R.id.home => 29 | finish() 30 | false 31 | case _ => super.onOptionsItemSelected(item) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/pathmorphing/Layout.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.pathmorphing 2 | 3 | import android.graphics.Color 4 | import android.view.View 5 | import android.widget._ 6 | import com.fortysevendeg.scala.android.R 7 | import com.fortysevendeg.macroid.extras.SeekBarEventsExtras.OnSeekBarChangeListenerHandler 8 | import com.fortysevendeg.macroid.extras.SeekBarTweaks._ 9 | import com.fortysevendeg.macroid.extras.TextTweaks._ 10 | import com.fortysevendeg.macroid.extras.ToolbarTweaks._ 11 | import com.fortysevendeg.macroid.extras.ViewTweaks._ 12 | import com.fortysevendeg.scala.android.ui.commons.ToolbarLayout 13 | import com.fortysevendeg.scala.android.ui.components.PathMorphDrawableTweaks._ 14 | import com.fortysevendeg.scala.android.ui.components.Dim 15 | import com.fortysevendeg.scala.android.ui.components.IconTypes._ 16 | import macroid.FullDsl._ 17 | import macroid.{ActivityContextWrapper, Transformer} 18 | import scala.language.postfixOps 19 | 20 | trait Layout 21 | extends ToolbarLayout 22 | with Styles { 23 | 24 | val sizeOptionList = List(Dim(48, 48), Dim(72, 72), Dim(96, 96), Dim(128, 128)) 25 | 26 | var icon = slot[ImageView] 27 | var sampleColor1Icon = slot[ImageView] 28 | var sampleColor2Icon = slot[ImageView] 29 | var sampleColor3Icon = slot[ImageView] 30 | var sampleColor4Icon = slot[ImageView] 31 | var iconSelectorGroup = slot[LinearLayout] 32 | var colorSelectorGroup = slot[LinearLayout] 33 | var strokeTitle = slot[TextView] 34 | var sizeTitle = slot[TextView] 35 | var strokeSelector = slot[SeekBar] 36 | var sizeSelector = slot[SeekBar] 37 | 38 | val deactivateImageViewWidgets = Transformer { 39 | case i: ImageView => i <~ vActivated(false) 40 | } 41 | 42 | val setNoIconImageViewWidgets = Transformer { 43 | case i: ImageView => i <~ pmdAnimIcon(NOICON) 44 | } 45 | 46 | def layout(implicit context: ActivityContextWrapper) = { 47 | getUi( 48 | l[LinearLayout]( 49 | toolBarLayout <~ tbTitle(R.string.title_path_morphing), 50 | l[LinearLayout]( 51 | l[FrameLayout]( 52 | w[ImageView] <~ drawableStyle(width = 48 dp, height = 48 dp, stroke = 3 dp) <~ wire(icon) 53 | ) <~ drawableContentStyle, 54 | l[ScrollView]( 55 | l[LinearLayout]( 56 | w[TextView] <~ tvText(R.string.title_select_icon) <~ titleStyle, 57 | l[LinearLayout]( 58 | l[LinearLayout]( 59 | w[ImageView] <~ iconSelectorStyle(BURGER, false) <~ FuncOn.click { 60 | (view: View) => { 61 | (iconSelectorGroup <~ deactivateImageViewWidgets) ~ 62 | (icon <~ pmdAnimIcon(BURGER)) ~ 63 | (view <~ vActivated(true)) 64 | } 65 | }, 66 | w[ImageView] <~ iconSelectorStyle(CHECK, false) <~ FuncOn.click { 67 | (view: View) => { 68 | (iconSelectorGroup <~ deactivateImageViewWidgets) ~ 69 | (icon <~ pmdAnimIcon(CHECK)) ~ 70 | (view <~ vActivated(true)) 71 | } 72 | }, 73 | w[ImageView] <~ iconSelectorStyle(ADD, false) <~ FuncOn.click { 74 | (view: View) => { 75 | (iconSelectorGroup <~ deactivateImageViewWidgets) ~ 76 | (icon <~ pmdAnimIcon(ADD)) ~ 77 | (view <~ vActivated(true)) 78 | } 79 | }, 80 | w[ImageView] <~ iconSelectorStyle(CLOSE, false) <~ FuncOn.click { 81 | (view: View) => { 82 | (iconSelectorGroup <~ deactivateImageViewWidgets) ~ 83 | (icon <~ pmdAnimIcon(CLOSE)) ~ 84 | (view <~ vActivated(true)) 85 | } 86 | } 87 | ) <~ tableLayoutRowStyle, 88 | l[LinearLayout]( 89 | w[ImageView] <~ iconSelectorStyle(UP, false) <~ FuncOn.click { 90 | (view: View) => { 91 | (iconSelectorGroup <~ deactivateImageViewWidgets) ~ 92 | (icon <~ pmdAnimIcon(UP)) ~ 93 | (view <~ vActivated(true)) 94 | } 95 | }, 96 | w[ImageView] <~ iconSelectorStyle(NEXT, false) <~ FuncOn.click { 97 | (view: View) => { 98 | (iconSelectorGroup <~ deactivateImageViewWidgets) ~ 99 | (icon <~ pmdAnimIcon(NEXT)) ~ 100 | (view <~ vActivated(true)) 101 | } 102 | }, 103 | w[ImageView] <~ iconSelectorStyle(DOWN, false) <~ FuncOn.click { 104 | (view: View) => { 105 | (iconSelectorGroup <~ deactivateImageViewWidgets) ~ 106 | (icon <~ pmdAnimIcon(DOWN)) ~ 107 | (view <~ vActivated(true)) 108 | } 109 | }, 110 | w[ImageView] <~ iconSelectorStyle(BACK, false) <~ FuncOn.click { 111 | (view: View) => { 112 | (iconSelectorGroup <~ deactivateImageViewWidgets) ~ 113 | (icon <~ pmdAnimIcon(BACK)) ~ 114 | (view <~ vActivated(true)) 115 | } 116 | } 117 | ) <~ tableLayoutRowStyle 118 | ) <~ tableLayoutStyle <~ wire(iconSelectorGroup), 119 | w[TextView] <~ tvText(R.string.title_select_color) <~ titleStyle, 120 | l[LinearLayout]( 121 | l[LinearLayout]( 122 | w[ImageView] <~ colorSelectorStyle(true) <~ wire(sampleColor1Icon) <~ pmdColor(Color.WHITE) <~ vBackground(R.drawable.background_item_pathmorphsample_1) <~ On.click { 123 | (icon <~ pmdColorResource(R.color.path_morph_sample_1)) ~ 124 | (colorSelectorGroup <~ setNoIconImageViewWidgets) ~ 125 | (sampleColor1Icon <~ pmdAnimIcon(CHECK)) 126 | }, 127 | w[ImageView] <~ colorSelectorStyle(false) <~ wire(sampleColor2Icon) <~ vBackground(R.drawable.background_item_pathmorphsample_2) <~ On.click { 128 | (icon <~ pmdColorResource(R.color.path_morph_sample_2)) ~ 129 | (colorSelectorGroup <~ setNoIconImageViewWidgets) ~ 130 | (sampleColor2Icon <~ pmdAnimIcon(CHECK)) 131 | }, 132 | w[ImageView] <~ colorSelectorStyle(false) <~ wire(sampleColor3Icon) <~ vBackground(R.drawable.background_item_pathmorphsample_3) <~ On.click { 133 | (icon <~ pmdColorResource(R.color.path_morph_sample_3)) ~ 134 | (colorSelectorGroup <~ setNoIconImageViewWidgets) ~ 135 | (sampleColor3Icon <~ pmdAnimIcon(CHECK)) 136 | }, 137 | w[ImageView] <~ colorSelectorStyle(false) <~ wire(sampleColor4Icon) <~ pmdColor(Color.WHITE) <~ vBackground(R.drawable.background_item_pathmorphsample_4) <~ On.click { 138 | (icon <~ pmdColorResource(R.color.path_morph_sample_4)) ~ 139 | (colorSelectorGroup <~ setNoIconImageViewWidgets) ~ 140 | (sampleColor4Icon <~ pmdAnimIcon(CHECK)) 141 | } 142 | ) <~ tableLayoutRowStyle 143 | ) <~ tableLayoutStyle <~ wire(colorSelectorGroup), 144 | w[TextView] <~ wire(strokeTitle) <~ tvText(R.string.title_select_stroke) <~ titleStyle, 145 | w[SeekBar] <~ wire(strokeSelector) <~ strokeStyle <~ sbOnSeekBarChangeListener( 146 | OnSeekBarChangeListenerHandler( 147 | onProgressChangedHandler = (view: SeekBar, progress: Int, fromUser: Boolean) => { 148 | val stroke = progress + 1 149 | 150 | (strokeTitle <~ tvText(context.application.getResources().getString(R.string.title_select_stroke, stroke.toString))) ~ 151 | (icon <~ pmdStroke(stroke dp)) 152 | } 153 | ) 154 | ), 155 | w[TextView] <~ wire(sizeTitle) <~ tvText(R.string.title_select_size) <~ titleStyle, 156 | w[SeekBar] <~ wire(sizeSelector) <~ sizeStyle <~ sbOnSeekBarChangeListener( 157 | OnSeekBarChangeListenerHandler( 158 | onStopTrackingTouchHandler = (view: SeekBar) => { 159 | val Dim(width, height) = sizeOptionList(view.getProgress()) 160 | 161 | (sizeTitle <~ tvText(context.application.getResources().getString(R.string.title_select_size, width.toString, height.toString))) ~ 162 | (colorSelectorGroup <~ setNoIconImageViewWidgets) ~ 163 | (sampleColor1Icon <~ pmdAnimIcon(CHECK)) ~ 164 | (strokeSelector <~ sbProgress(2)) ~ 165 | (icon <~ drawableStyle(width dp, height dp, 3 dp) <~ pmdColorResource(R.color.path_morph_sample_1)) ~ 166 | (iconSelectorGroup <~ deactivateImageViewWidgets) 167 | } 168 | ) 169 | ) 170 | ) <~ verticalLinearLayoutStyle 171 | ) <~ scrollViewStyle 172 | ) <~ contentStyle 173 | ) <~ rootStyle 174 | ) 175 | } 176 | 177 | } -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/pathmorphing/PathMorphingActivity.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.pathmorphing 2 | 3 | import android.os.Bundle 4 | import android.support.v7.app.AppCompatActivity 5 | import android.view.MenuItem 6 | import com.fortysevendeg.scala.android.R 7 | import com.fortysevendeg.macroid.extras.SeekBarTweaks._ 8 | import com.fortysevendeg.macroid.extras.TextTweaks._ 9 | import com.fortysevendeg.scala.android.ui.components.PathMorphDrawableTweaks._ 10 | import com.fortysevendeg.scala.android.ui.components.IconTypes._ 11 | import macroid.Contexts 12 | import macroid.FullDsl._ 13 | 14 | class PathMorphingActivity 15 | extends AppCompatActivity 16 | with Contexts[AppCompatActivity] 17 | with Layout { 18 | 19 | override def onCreate(savedInstanceState: Bundle) = { 20 | super.onCreate(savedInstanceState) 21 | 22 | setContentView(layout) 23 | 24 | toolBar foreach setSupportActionBar 25 | 26 | getSupportActionBar.setDisplayHomeAsUpEnabled(true) 27 | 28 | runUi( 29 | (icon <~ pmdAnimIcon(BURGER)) ~ 30 | (strokeSelector <~ sbProgress(2)) ~ 31 | (sizeTitle <~ tvText(getString(R.string.title_select_size, 48.toString, 48.toString))) 32 | ) 33 | } 34 | 35 | override def onOptionsItemSelected(item: MenuItem): Boolean = item.getItemId match { 36 | case android.R.id.home => 37 | finish() 38 | false 39 | case _ => super.onOptionsItemSelected(item) 40 | } 41 | } -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/pathmorphing/Styles.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.pathmorphing 2 | 3 | import android.view.Gravity 4 | import android.widget._ 5 | import com.fortysevendeg.scala.android.R 6 | import com.fortysevendeg.macroid.extras.ImageViewTweaks._ 7 | import com.fortysevendeg.macroid.extras.LinearLayoutTweaks._ 8 | import com.fortysevendeg.macroid.extras.FrameLayoutTweaks._ 9 | import com.fortysevendeg.macroid.extras.ResourcesExtras._ 10 | import com.fortysevendeg.macroid.extras.SeekBarTweaks._ 11 | import com.fortysevendeg.macroid.extras.TextTweaks._ 12 | import com.fortysevendeg.macroid.extras.ViewTweaks._ 13 | import com.fortysevendeg.scala.android.ui.components.PathMorphDrawable 14 | import com.fortysevendeg.scala.android.ui.components.IconTypes._ 15 | import macroid.{Tweak, ContextWrapper} 16 | import macroid.FullDsl._ 17 | import scala.language.postfixOps 18 | 19 | trait Styles { 20 | 21 | val rootStyle: Tweak[LinearLayout] = 22 | vMatchParent + 23 | llVertical 24 | 25 | val contentStyle: Tweak[LinearLayout] = 26 | vMatchParent + 27 | llVertical 28 | 29 | def scrollViewStyle(implicit context: ContextWrapper): Tweak[ScrollView] = 30 | vMatchParent + 31 | vPaddings(resGetDimensionPixelSize(R.dimen.padding_default)) 32 | 33 | val horizontalLinearLayoutStyle: Tweak[LinearLayout] = llHorizontal 34 | 35 | val verticalLinearLayoutStyle: Tweak[LinearLayout] = 36 | vMatchParent + 37 | llVertical 38 | 39 | val contentItemsRadiosStyle: Tweak[LinearLayout] = llMatchWeightHorizontal 40 | 41 | val titleStyle: Tweak[TextView] = tvNormalLight 42 | 43 | val strokeStyle: Tweak[SeekBar] = 44 | sbMax(4) + 45 | sbProgress(0) + 46 | vMatchWidth 47 | 48 | val sizeStyle: Tweak[SeekBar] = 49 | sbMax(3) + 50 | sbProgress(0) + 51 | vMatchWidth 52 | 53 | def drawableContentStyle(implicit context: ContextWrapper): Tweak[FrameLayout] = { 54 | val size = resGetDimensionPixelSize(R.dimen.path_morphing_size_content_drawable) 55 | lp[LinearLayout](size, size) + 56 | llLayoutGravity(Gravity.CENTER) 57 | } 58 | 59 | 60 | def drawableStyle(width: Int, height: Int, stroke: Int)(implicit context: ContextWrapper): Tweak[ImageView] = 61 | lp[LinearLayout](width, height) + 62 | flLayoutGravity(Gravity.CENTER) + 63 | vMargins(resGetDimensionPixelSize(R.dimen.padding_default)) + 64 | ivSrc(new PathMorphDrawable(defaultStroke = stroke)) + 65 | vBackground(R.drawable.background_item_square) 66 | 67 | def colorSelectorStyle(selected: Boolean = false)(implicit context: ContextWrapper): Tweak[ImageView] = { 68 | val size = resGetDimensionPixelSize(R.dimen.path_morphing_size_circles) 69 | lp[LinearLayout](size, size) + 70 | vPaddings(8 dp) + 71 | vMargins(8 dp) + 72 | (selected match { 73 | case true => ivSrc(new PathMorphDrawable(defaultIcon = CHECK, defaultStroke = 2 dp)) 74 | case _ => ivSrc(new PathMorphDrawable(defaultStroke = 2 dp)) 75 | }) 76 | } 77 | 78 | def iconSelectorStyle(icon: Int, selected: Boolean = false)(implicit context: ContextWrapper): Tweak[ImageView] = { 79 | val size = resGetDimensionPixelSize(R.dimen.path_morphing_size_circles) 80 | lp[LinearLayout](size, size) + 81 | vPaddings(resGetDimensionPixelSize(R.dimen.padding_default)) + 82 | vBackground(R.drawable.background_item_icon_selector) + 83 | vMargins(resGetDimensionPixelSize(R.dimen.padding_default)) + 84 | ivSrc(new PathMorphDrawable(defaultIcon = icon, defaultStroke = 2 dp)) + 85 | vActivated(selected) 86 | } 87 | 88 | val tableLayoutStyle: Tweak[LinearLayout] = 89 | vMatchWidth + 90 | llVertical 91 | 92 | def tableLayoutRowStyle(implicit context: ContextWrapper): Tweak[LinearLayout] = 93 | vMatchWidth + 94 | llHorizontal + 95 | llGravity(Gravity.CENTER) 96 | } 97 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/ripplebackground/Layout.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.ripplebackground 2 | 3 | import android.widget.{FrameLayout, LinearLayout} 4 | import com.fortysevendeg.scala.android.R 5 | import com.fortysevendeg.scala.android.ui.commons.ToolbarLayout 6 | import com.fortysevendeg.scala.android.ui.components.{CircleView, RippleBackgroundView} 7 | import Styles._ 8 | import macroid.ActivityContextWrapper 9 | import macroid.FullDsl._ 10 | import com.fortysevendeg.macroid.extras.ToolbarTweaks._ 11 | 12 | trait Layout extends ToolbarLayout { 13 | 14 | var rippleBackground = slot[RippleBackgroundView] 15 | 16 | var circle1 = slot[CircleView] 17 | 18 | var circle2 = slot[CircleView] 19 | 20 | var circle3 = slot[CircleView] 21 | 22 | def layout(implicit context: ActivityContextWrapper) = getUi( 23 | l[LinearLayout]( 24 | toolBarLayout <~ tbTitle(R.string.title_ripple_background), 25 | l[FrameLayout]( 26 | w[RippleBackgroundView] <~ wire(rippleBackground) <~ backgroundStyle, 27 | l[LinearLayout]( 28 | w[CircleView] <~ wire(circle1) <~ circleStyle, 29 | w[CircleView] <~ wire(circle2) <~ circleStyle, 30 | w[CircleView] <~ wire(circle3) <~ circleStyle 31 | ) <~ circlesContentStyle 32 | ) <~ colorContent 33 | ) <~ rootStyle 34 | ) 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/ripplebackground/RippleBackgroundActivity.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.ripplebackground 2 | 3 | import android.os.Bundle 4 | import android.support.v7.app.AppCompatActivity 5 | import android.view.MenuItem 6 | import com.fortysevendeg.macroid.extras.MoveSnails._ 7 | import com.fortysevendeg.macroid.extras.ResourcesExtras._ 8 | import com.fortysevendeg.macroid.extras.ViewTweaks._ 9 | import com.fortysevendeg.scala.android.R 10 | import com.fortysevendeg.scala.android.ui.components.CircleView._ 11 | import com.fortysevendeg.scala.android.ui.components.RippleBackgroundSnails._ 12 | import com.fortysevendeg.scala.android.ui.components.{CircleView, RippleSnailData} 13 | import macroid.FullDsl._ 14 | import macroid.{Contexts, Ui} 15 | 16 | import scala.concurrent.ExecutionContext.Implicits.global 17 | 18 | class RippleBackgroundActivity 19 | extends AppCompatActivity 20 | with Contexts[AppCompatActivity] 21 | with Layout { 22 | 23 | override def onCreate(savedInstanceState: Bundle) = { 24 | super.onCreate(savedInstanceState) 25 | 26 | setContentView(layout) 27 | 28 | val color1 = resGetColor(R.color.ripple_bg_color1) 29 | val color2 = resGetColor(R.color.ripple_bg_color2) 30 | val color3 = resGetColor(R.color.ripple_bg_color3) 31 | 32 | runUi( 33 | (rippleBackground <~ vBackgroundColor(color1)) ~ 34 | (circle1 <~ cvColor(color1) <~ On.click(anim(circle1, color1))) ~ 35 | (circle2 <~ cvColor(color2) <~ On.click(anim(circle2, color2))) ~ 36 | (circle3 <~ cvColor(color3) <~ On.click(anim(circle3, color3))) 37 | ) 38 | 39 | toolBar foreach setSupportActionBar 40 | 41 | getSupportActionBar.setDisplayHomeAsUpEnabled(true) 42 | 43 | } 44 | 45 | override def onOptionsItemSelected(item: MenuItem): Boolean = item.getItemId match { 46 | case android.R.id.home => 47 | finish() 48 | false 49 | case _ => super.onOptionsItemSelected(item) 50 | } 51 | 52 | def anim(circleView: Option[CircleView], color: Int): Ui[_] = rippleBackground map { 53 | background ⇒ 54 | val rippleData = RippleSnailData.toCenterView(background). 55 | copy( 56 | resColor = color, 57 | onAnimationEnd = Some { 58 | () ⇒ 59 | runUi(circleView <~ vTransformation(0, 0)) 60 | } 61 | ) 62 | (circleView <~~ move(rippleBackground)) ~~ (rippleBackground <~~ ripple(rippleData)) ~~ (circleView <~~ fadeIn(1000)) 63 | } getOrElse Ui.nop 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/ripplebackground/Styles.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.ripplebackground 2 | 3 | import android.view.Gravity 4 | import android.view.ViewGroup.LayoutParams._ 5 | import android.widget.{FrameLayout, LinearLayout} 6 | import com.fortysevendeg.macroid.extras.LinearLayoutTweaks._ 7 | import com.fortysevendeg.macroid.extras.ResourcesExtras._ 8 | import com.fortysevendeg.macroid.extras.ViewTweaks._ 9 | import com.fortysevendeg.scala.android.R 10 | import com.fortysevendeg.scala.android.ui.components.{CircleView, RippleBackgroundView} 11 | import macroid.FullDsl._ 12 | import macroid.{ContextWrapper, Tweak} 13 | 14 | import scala.language.postfixOps 15 | 16 | object Styles { 17 | 18 | val rootStyle: Tweak[LinearLayout] = 19 | vMatchParent + 20 | llVertical 21 | 22 | val colorContent: Tweak[FrameLayout] = 23 | llMatchWeightVertical 24 | 25 | def backgroundStyle(implicit context: ContextWrapper): Tweak[RippleBackgroundView] = { 26 | val height = resGetDimensionPixelSize(R.dimen.ripple_bg_height_content) 27 | lp[LinearLayout](MATCH_PARENT, height) 28 | } 29 | 30 | def circlesContentStyle(implicit context: ContextWrapper): Tweak[LinearLayout] = { 31 | vMatchWidth + 32 | llGravity(Gravity.CENTER_HORIZONTAL) + 33 | llHorizontal 34 | } 35 | 36 | def circleStyle(implicit context: ContextWrapper): Tweak[CircleView] = { 37 | val size = resGetDimensionPixelSize(R.dimen.ripple_bg_size_circle) 38 | val margin = resGetDimensionPixelSize(R.dimen.padding_default) 39 | val marginTop = resGetDimensionPixelSize(R.dimen.ripple_bg_height_content) 40 | lp[LinearLayout](size, size) + 41 | vMargin( 42 | marginLeft = margin, 43 | marginTop = margin + marginTop, 44 | marginRight = margin, 45 | marginBottom = margin 46 | ) 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/textstyles/Layout.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.textstyles 2 | 3 | import android.widget.{LinearLayout, ScrollView, TextView} 4 | import com.fortysevendeg.scala.android.R 5 | import com.fortysevendeg.macroid.extras.ToolbarTweaks._ 6 | import com.fortysevendeg.scala.android.ui.commons.ToolbarLayout 7 | import macroid.FullDsl._ 8 | import macroid.ActivityContextWrapper 9 | 10 | trait Layout 11 | extends ToolbarLayout 12 | with Styles { 13 | 14 | def layout(implicit context: ActivityContextWrapper) = { 15 | getUi( 16 | l[LinearLayout]( 17 | toolBarLayout <~ tbTitle(R.string.title_text_styles), 18 | l[ScrollView]( 19 | l[LinearLayout]( 20 | w[TextView] <~ textLargeStyle <~ text("Text Large"), 21 | w[TextView] <~ textMediumStyle <~ text("Text Medium"), 22 | w[TextView] <~ textSmallStyle <~ text("Text Small"), 23 | w[TextView] <~ textLightLargeStyle <~ text("Text Light Large"), 24 | w[TextView] <~ textLightMediumStyle <~ text("Text Light Medium"), 25 | w[TextView] <~ textLightSmallStyle <~ text("Text Light Small"), 26 | w[TextView] <~ textBoldCondensedLargeStyle <~ text("Text Bold Condensed Large"), 27 | w[TextView] <~ textBoldCondensedMediumStyle <~ text("Text Bold Condensed Medium"), 28 | w[TextView] <~ textBoldCondensedSmallStyle <~ text("Text Bold Condensed Small"), 29 | w[TextView] <~ textItalicLargeStyle <~ text("Text Italic Large"), 30 | w[TextView] <~ textItalicMediumStyle <~ text("Text Italic Medium"), 31 | w[TextView] <~ textItalicSmallStyle <~ text("Text Italic Small") 32 | ) <~ scrollStyle 33 | ) 34 | ) <~ contentStyle 35 | ) 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/textstyles/Styles.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.textstyles 2 | 3 | import com.fortysevendeg.scala.android.R 4 | import com.fortysevendeg.macroid.extras.LinearLayoutTweaks._ 5 | import com.fortysevendeg.macroid.extras.ResourcesExtras._ 6 | import com.fortysevendeg.macroid.extras.TextTweaks._ 7 | import com.fortysevendeg.macroid.extras.ViewTweaks._ 8 | import macroid.ContextWrapper 9 | import scala.language.postfixOps 10 | 11 | trait Styles { 12 | 13 | val contentStyle = llVertical 14 | 15 | def scrollStyle(implicit context: ContextWrapper) = 16 | llVertical + 17 | vPaddings(resGetDimensionPixelSize(R.dimen.padding_default_large)) 18 | 19 | def textLargeStyle(implicit context: ContextWrapper) = 20 | tvSizeResource(R.dimen.font_size_large) + 21 | vPaddings(resGetDimensionPixelSize(R.dimen.padding_default)) 22 | 23 | def textMediumStyle(implicit context: ContextWrapper) = 24 | tvSizeResource(R.dimen.font_size_medium) + 25 | vPaddings(resGetDimensionPixelSize(R.dimen.padding_default)) 26 | 27 | def textSmallStyle(implicit context: ContextWrapper) = 28 | tvSizeResource(R.dimen.font_size_small) + 29 | vPaddings(resGetDimensionPixelSize(R.dimen.padding_default)) 30 | 31 | def textLightLargeStyle(implicit context: ContextWrapper) = 32 | tvSizeResource(R.dimen.font_size_large) + 33 | tvNormalLight + 34 | vPaddings(resGetDimensionPixelSize(R.dimen.padding_default)) 35 | 36 | def textLightMediumStyle(implicit context: ContextWrapper) = 37 | tvSizeResource(R.dimen.font_size_medium) + 38 | tvNormalLight + 39 | vPaddings(resGetDimensionPixelSize(R.dimen.padding_default)) 40 | 41 | def textLightSmallStyle(implicit context: ContextWrapper) = 42 | tvSizeResource(R.dimen.font_size_small) + 43 | tvNormalLight + 44 | vPaddings(resGetDimensionPixelSize(R.dimen.padding_default)) 45 | 46 | def textBoldCondensedLargeStyle(implicit context: ContextWrapper) = 47 | tvSizeResource(R.dimen.font_size_large) + 48 | tvBoldCondensed + 49 | vPaddings(resGetDimensionPixelSize(R.dimen.padding_default)) 50 | 51 | def textBoldCondensedMediumStyle(implicit context: ContextWrapper) = 52 | tvSizeResource(R.dimen.font_size_medium) + 53 | tvBoldCondensed + 54 | vPaddings(resGetDimensionPixelSize(R.dimen.padding_default)) 55 | 56 | def textBoldCondensedSmallStyle(implicit context: ContextWrapper) = 57 | tvSizeResource(R.dimen.font_size_small) + 58 | tvBoldCondensed + 59 | vPaddings(resGetDimensionPixelSize(R.dimen.padding_default)) 60 | 61 | def textItalicLargeStyle(implicit context: ContextWrapper) = 62 | tvSizeResource(R.dimen.font_size_large) + 63 | tvItalic + 64 | vPaddings(resGetDimensionPixelSize(R.dimen.padding_default)) 65 | 66 | def textItalicMediumStyle(implicit context: ContextWrapper) = 67 | tvSizeResource(R.dimen.font_size_medium) + 68 | tvItalic + 69 | vPaddings(resGetDimensionPixelSize(R.dimen.padding_default)) 70 | 71 | def textItalicSmallStyle(implicit context: ContextWrapper) = 72 | tvSizeResource(R.dimen.font_size_small) + 73 | tvItalic + 74 | vPaddings(resGetDimensionPixelSize(R.dimen.padding_default)) 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/main/scala/com/fortysevendeg/scala/android/ui/textstyles/TextStylesActivity.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.ui.textstyles 2 | 3 | import android.os.Bundle 4 | import android.support.v7.app.AppCompatActivity 5 | import android.view.MenuItem 6 | import macroid.Contexts 7 | 8 | class TextStylesActivity 9 | extends AppCompatActivity 10 | with Contexts[AppCompatActivity] 11 | with Layout { 12 | 13 | override def onCreate(savedInstanceState: Bundle) = { 14 | super.onCreate(savedInstanceState) 15 | 16 | setContentView(layout) 17 | 18 | toolBar map setSupportActionBar 19 | 20 | getSupportActionBar.setDisplayHomeAsUpEnabled(true) 21 | 22 | } 23 | 24 | override def onOptionsItemSelected(item: MenuItem): Boolean = { 25 | item.getItemId match { 26 | case android.R.id.home => { 27 | finish() 28 | false 29 | } 30 | } 31 | super.onOptionsItemSelected(item) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/test/resources/weather.json: -------------------------------------------------------------------------------- 1 | { 2 | "coord": { 3 | "lon": -122.38, 4 | "lat": 47.66 5 | }, 6 | "sys": { 7 | "message": 0.0168, 8 | "country": "US", 9 | "sunrise": 1425048757, 10 | "sunset": 1425088297 11 | }, 12 | "weather": [ 13 | { 14 | "id": 501, 15 | "main": "Rain", 16 | "description": "moderate rain", 17 | "icon": "10d" 18 | } 19 | ], 20 | "base": "cmc stations", 21 | "main": { 22 | "temp": 6.636, 23 | "temp_min": 6.636, 24 | "temp_max": 6.636, 25 | "pressure": 1009.8, 26 | "sea_level": 1021.74, 27 | "grnd_level": 1009.8, 28 | "humidity": 100 29 | }, 30 | "wind": { 31 | "speed": 2.17, 32 | "deg": 36.0011 33 | }, 34 | "clouds": { 35 | "all": 92 36 | }, 37 | "rain": { 38 | "3h": 3.5 39 | }, 40 | "dt": 1425060066, 41 | "id": 5809844, 42 | "name": "Seattle", 43 | "cod": 200 44 | } 45 | -------------------------------------------------------------------------------- /src/test/scala/com/fortysevendeg/scala/android/BaseTestSpecification.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android 2 | 3 | import org.specs2.mutable.Specification 4 | import org.specs2.specification.Scope 5 | 6 | import scala.concurrent.duration.Duration 7 | import scala.concurrent.{Await, Future} 8 | 9 | trait BaseTestSpecification extends Specification { 10 | 11 | def await[T](future: Future[T]) = Await.result(future, Duration.Inf) 12 | 13 | trait BaseTestScope extends Scope 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/test/scala/com/fortysevendeg/scala/android/ContextWrapperContextTestSupport.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android 2 | 3 | import android.content.Context 4 | import macroid.ContextWrapper 5 | import org.specs2.mock.Mockito 6 | 7 | trait ContextWrapperContextTestSupport 8 | extends Mockito { 9 | 10 | val mockContext = mock[Context] 11 | 12 | implicit val context: ContextWrapper = mock[ContextWrapper] 13 | 14 | context.application returns mockContext 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/test/scala/com/fortysevendeg/scala/android/modules/forecast/ForecastServicesSpec.scala: -------------------------------------------------------------------------------- 1 | package com.fortysevendeg.scala.android.modules.forecast 2 | 3 | import com.fortysevendeg.scala.android.modules.forecast.impl.ForecastServices 4 | import com.fortysevendeg.scala.android.modules.forecast.model._ 5 | import com.fortysevendeg.scala.android.{ContextWrapperContextTestSupport, BaseTestSpecification} 6 | import com.squareup.okhttp.OkHttpClient 7 | import io.taig.communicator.result.Parser 8 | import macroid.ContextWrapper 9 | 10 | import scala.concurrent.duration.Duration 11 | import scala.concurrent.{Await, Future} 12 | 13 | trait ForecastServicesSpecification 14 | extends BaseTestSpecification 15 | with ContextWrapperContextTestSupport { 16 | 17 | trait ForecastServicesScope 18 | extends BaseTestScope 19 | with ForecastServices 20 | with ForecastServicesData { 21 | 22 | override def loadJsonUrl(latitude: Double, longitude: Double)(implicit context: ContextWrapper): String = "http://fake_url/" 23 | 24 | override def loadHeaderTuple(implicit context: ContextWrapper): (String, String) = ("key_name", "key_value") 25 | 26 | } 27 | 28 | } 29 | 30 | trait ForecastServicesData { 31 | 32 | val validJson: ApiModel = ApiModel( 33 | id = 5809844, 34 | dt = 1425060066, 35 | base = "cmc stations", 36 | cod = 200, 37 | name = "Seattle", 38 | coord = ApiCoord(-122.38, 47.66), 39 | sys = ApiSys( 40 | message = Some(0.0168), 41 | country = Some("US"), 42 | sunrise = Some(1425048757), 43 | sunset = Some(1425088297)), 44 | main = ApiMain( 45 | temp = Some(6.636), 46 | temp_min = Some(6.636), 47 | temp_max = Some(6.636), 48 | pressure = Some(1009.8), 49 | sea_level = Some(1021.71), 50 | grnd_level = Some(1009.8), 51 | humidity = Some(100)), 52 | weather = Seq(ApiWeather(id = 501, main = "Rain", description = "moderate rain", icon = "10d")), 53 | wind = ApiWind(speed = Some(2.17), deg = Some(36.0011)), 54 | rain = Some(Map("3h" -> 3.5)), 55 | clouds = ApiClouds(all = Some(92))) 56 | 57 | } 58 | 59 | class ForecastServicesSpec extends ForecastServicesSpecification { 60 | 61 | "ForecastServices component" should { 62 | 63 | "return forecast with right JSON" in 64 | new ForecastServicesScope { 65 | 66 | override def loadJson[T](url: String, headers: Seq[(String, String)])(implicit parser: Parser[T], client: OkHttpClient = new OkHttpClient()): Future[T] = 67 | Future.successful[T](validJson.asInstanceOf[T]) 68 | 69 | val forecast = Forecast( 70 | Location(5809844, "Seattle", 47.66, -122.38), 71 | Some(Weather(501, "Rain", "moderate rain", "10d", Some(6.636)))) 72 | 73 | Await.result(loadForecast(ForecastRequest(0, 0)), Duration.Inf) shouldEqual ForecastResponse(Some(forecast)) 74 | } 75 | 76 | "got the exception thrown by the call to loadJson" in 77 | new ForecastServicesScope { 78 | 79 | override def loadJson[T](url: String, headers: Seq[(String, String)])(implicit parser: Parser[T], client: OkHttpClient = new OkHttpClient()): Future[T] = 80 | Future.failed[T](new RuntimeException()) 81 | 82 | Await.result(loadForecast(ForecastRequest(0, 0)), Duration.Inf) must throwA[RuntimeException] 83 | } 84 | 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/test/scala/com/fortysevendeg/scala/android/modules/forecast/model/JsonModelSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 47 Degrees, LLC http://47deg.com hello@47deg.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | * not use this file except in compliance with the License. You may obtain 6 | * a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.fortysevendeg.scala.android.modules.forecast.model 18 | 19 | import com.fortysevendeg.scala.android.BaseTestSpecification 20 | import play.api.libs.json.{JsValue, Json} 21 | import JsonImplicits._ 22 | 23 | object JsonImplicits { 24 | 25 | implicit val apiCloudsReads = Json.reads[ApiClouds] 26 | implicit val apiWindReads = Json.reads[ApiWind] 27 | implicit val apiWeatherReads = Json.reads[ApiWeather] 28 | implicit val apiMainReads = Json.reads[ApiMain] 29 | implicit val apiSysReads = Json.reads[ApiSys] 30 | implicit val apiCoordReads = Json.reads[ApiCoord] 31 | implicit val apiModelReads = Json.reads[ApiModel] 32 | 33 | } 34 | 35 | class JsonModelSpec 36 | extends BaseTestSpecification { 37 | 38 | "load and map sample json" should { 39 | 40 | "return an ApiModel class with the right fields" in new BaseTestScope { 41 | 42 | 43 | val jsonSource = scala.io.Source.fromInputStream(JsonImplicits.getClass.getResourceAsStream("/weather.json")).mkString 44 | val json: JsValue = Json.parse(jsonSource) 45 | val jsonValue = json.as[ApiModel] 46 | 47 | jsonValue.weather.size shouldEqual 1 48 | jsonValue.name shouldEqual "Seattle" 49 | } 50 | 51 | } 52 | 53 | } 54 | --------------------------------------------------------------------------------