Repository: Actinarium/Rhythm Branch: master Commit: ace2146ee9f2 Files: 64 Total size: 287.1 KB Directory structure: gitextract_5u5_66cj/ ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── rhythm/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── com/ │ └── actinarium/ │ └── rhythm/ │ ├── AbstractSpecLayerGroup.java │ ├── ArgumentsBundle.java │ ├── MagicVariablesArgumentsBundle.java │ ├── RhythmDrawable.java │ ├── RhythmInflationException.java │ ├── RhythmOverlay.java │ ├── RhythmOverlayInflater.java │ ├── RhythmSpecLayer.java │ ├── RhythmSpecLayerFactory.java │ ├── RhythmSpecLayerParent.java │ ├── SimpleArgumentsBundle.java │ ├── SimpleCacheFactory.java │ ├── internal/ │ │ ├── ReaderUtils.java │ │ └── RuntimeIOException.java │ └── layer/ │ ├── Columns.java │ ├── DimensionsLabel.java │ ├── Fill.java │ ├── GridLines.java │ ├── Inset.java │ ├── Keyline.java │ └── RatioKeyline.java ├── rhythm-control/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── actinarium/ │ │ └── rhythm/ │ │ └── control/ │ │ ├── RhythmControl.java │ │ ├── RhythmFrameLayout.java │ │ ├── RhythmGroup.java │ │ └── RhythmNotificationService.java │ └── res/ │ └── values/ │ ├── attrs.xml │ └── strings.xml ├── sample/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ ├── sample-release.apk │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── actinarium/ │ │ └── rhythm/ │ │ └── sample/ │ │ ├── MainActivity.java │ │ ├── RhythmSampleApplication.java │ │ ├── RhythmSandbox.java │ │ ├── customlayers/ │ │ │ ├── ImageBox.java │ │ │ └── LayoutBounds.java │ │ └── util/ │ │ ├── BulletSpan.java │ │ └── ViewUtils.java │ └── res/ │ ├── layout/ │ │ ├── activity_main.xml │ │ └── overlay_sandbox.xml │ ├── layout-land/ │ │ └── overlay_sandbox.xml │ ├── raw/ │ │ └── overlay_config │ └── values/ │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .gradle /local.properties .DS_Store /build /captures *.iml .idea ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # Rhythm [![Discontinued](https://img.shields.io/badge/status-discontinued-red.svg)](#discontinued-library) [![Download from Bintray](https://api.bintray.com/packages/actinarium/maven/rhythm/images/download.svg)](https://bintray.com/actinarium/maven/rhythm/_latestVersion) [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-Rhythm-green.svg?maxAge=864000)](https://android-arsenal.com/details/1/2664) [![API](https://img.shields.io/badge/API-8%2B-brightgreen.svg?maxAge=864000)](https://android-arsenal.com/api?level=8) [![License: Apache 2.0](https://img.shields.io/github/license/actinarium/rhythm.svg?maxAge=864000)][license] Rhythm is a design overlay engine for Android. With Rhythm you can easily render grids, keylines, other [Material Design][mdspec] cues and even custom elements within your app, helping you to build perfect layouts. Define overlay configurations using [simple expression language][wiki-config], and Rhythm will convert them into Drawables¹, which you can then set as view backgrounds, foregrounds, draw to bitmaps etc: ![Simple Rhythm overlay example](http://actinarium.github.io/Rhythm/assets/rhythm-hero-small.png) ## Discontinued library Unfortunately the development of this library is discontinued. Such decision was made based on its usefulness vs maintenance effort. The library had insignificant if not zero downloads; it's only actively used in my own Material Cue app, and it's been getting harder and harder to mantain these two separately. Let us say, it served its purpose well as an interim step to get Material Cue done. Rhythm code will remain as is. All new features will go directly into Material Cue. If you're interested in revival of this library, ping me. ## Material Cue **Material Cue** is a standalone keyline app built on Rhythm. If you need to verify your layout but don’t want the trouble of setting up another library in your project, Material Cue is perfect for you. Give it a try: Get it on Google Play Learn more about the [differences between Rhythm and Material Cue](https://github.com/Actinarium/Rhythm/wiki/Comparison-of-Rhythm-and-Material-Cue). ## Quick setup Starting with 0.9.6, Rhythm is packaged as two separate artifacts: * **Rhythm Core** — contains core rendering framework for turning [human readable config][wiki-config] into Drawable objects¹. You then manage those Drawables yourself. * **Rhythm Control** — provides a mechanism to assign many overlays to many views and switch them on the go using the Rhythm Control notification. **Tip:** Look at the [sample app][samplesrc]. ### Rhythm Core 1. Add Gradle dependency: ``` compile 'com.actinarium.rhythm:rhythm:0.9.6' ``` For alternative setup (JAR, Maven) see [Bintray page][bintray]. 2. Create a raw file in your app’s `/src/res/raw` folder, e.g. `/src/res/raw/overlays`, with content like this: ``` # Standard 8dp grid grid-lines step=8dp from=top grid-lines step=8dp from=left # Typography grid w/keylines grid-lines step=4dp from=top keyline distance=16dp from=left keyline distance=16dp from=right keyline distance=72dp from=left ``` Overlays are separated by empty newline. Lines starting with `#` are optional overlay titles. There can also be comments and variables. > Take a look at the [sample config file][sampleconfig] for a more complex and documented example. For full docs see [the wiki][wiki-config]. 3. In your code, inflate this file into a list of overlay objects, wrap them with [RhythmDrawables](http://actinarium.github.io/Rhythm/javadoc/rhythm/com/actinarium/rhythm/RhythmDrawable.html), and assign to views as required: ```java RhythmOverlayInflater inflater = RhythmOverlayInflater.createDefault(context); List overlays = inflater.inflate(R.raw.overlays); Drawable overlayDrawable = new RhythmDrawable(overlays.get(1)); view.setBackground(overlayDrawable); ``` 4. Later you can replace the overlay in that drawable: ``` ((RhythmDrawable) view.getBackground()).setOverlay(overlays.get(0)); ``` or disable it: ``` ((RhythmDrawable) view.getBackground()).setOverlay(null); ``` ### Rhythm Control > This module is discontinued in favor of [Material Cue][mcue] app and will not receive new functionality. If you want to switch overlays for the views in your app at runtime, you can setup Rhythm Control. This module allows to define groups with many views and overlays attached to them, and then separately control which overlay is displayed over all views in a particular group. This can be done with a Rhythm Control notification², where you can cycle through the groups (1 › 2 › 3 › … › last › 1) and current group’s overlays (1 › 2 › 3 › … › last › no overlay › 1): ![Rhythm Control notification](http://actinarium.github.io/Rhythm/assets/rhythm-control-small.png) 1. Set up Rhythm Core: add a dependency and compose a configuration sheet. 2. Add another dependency for Rhythm Control: ``` compile 'com.actinarium.rhythm:rhythm-control:0.9.6' ``` For alternative setup (JAR, Maven) see [Bintray page][bintray]. 3. Implement `RhythmControl.Host` in your Application class (create one if it doesn’t exist yet): ```java public class MyApplication extends Application implements RhythmControl.Host { private RhythmControl mRhythmControl; @Override public void onCreate() { super.onCreate(); mRhythmControl = new RhythmControl(this); /* Rest of OnCreate() code */ } @Override public RhythmControl getRhythmControl() { return mRhythmControl; } } ``` 4. In your `Application.onCreate()` method set up Rhythm groups: ```java // Create an inflater and inflate overlays from your configuration sheet RhythmOverlayInflater inflater = RhythmOverlayInflater.createDefault(this); List overlays = inflater.inflate(R.raw.overlays); // Create the groups. Each group will be given an index starting from 0 RhythmGroup mainGroup = mRhythmControl.makeGroup("A group with index 0"); RhythmGroup secondaryGroup = mRhythmControl.makeGroup("Another group with index 1"); RhythmGroup anotherGroup = mRhythmControl.makeGroup("Group with index 2"); // Assign overlays to groups mainGroup.addOverlay(overlays.get(0)); // add only one secondaryGroup.addOverlays(overlays); // add all anotherGroup.addOverlays(overlays.subList(0, 2)); // add first two // Finally, display the notification with notification ID unique across your app // (to avoid conflicts with other notifications) mRhythmControl.showQuickControl(RHYTHM_NOTIFICATION_ID); ``` 5. Finally, integrate Rhythm groups into your layouts. There are two ways to do this: * Decorate existing views programmatically, e.g. in your `Activity.onCreate()` methods: ```java // Retrieve Rhythm Control from application class: RhythmControl rhythmControl = ((RhythmControl.Host) getApplication()).getRhythmControl(); // Decorate backgrounds of given views (draws overlay over existing background but under content) // Works with any views rhythmControl.getGroup(0).decorate(view1, view2, view3 /*, ... */); // Decorate foregrounds of given views - will draw overlay over content // Works with FrameLayouts and its subclasses (e.g. CardView) only rhythmControl.getGroup(1).decorateForeground(card1, frame2, card3 /*, ... */); ``` * Wrap pieces of your layouts with `RhythmFrameLayout` connected to appropriate groups: ```xml ``` **Protip:** you can use `RhythmFrameLayout` on its own, without setting up Rhythm control or groups. Omit the `app:rhythmGroup` attribute or set it to `app:rhythmGroup="noGroup"`, and set overlays to it programmatically: ```java // By default, RhythmFrameLayout doesn't have a RhythmDrawable object, so inject a new one rhythmFrameLayout.setRhythmDrawable(new RhythmDrawable(rhythmOverlay)); // After that you can change overlays in that drawable rhythmFrameLayout.getRhythmDrawable().setOverlay(anotherOverlay); ``` ### Further reading * [Documentation (wiki)][wiki] * [Rhythm Core javadoc][javadoc-core] * [Rhythm Control javadoc][javadoc-control] ## A personal appeal If you like what I’m doing, please consider supporting my efforts. I quit my full-time job so that I could focus on building useful, time-saving libraries and apps. I’ve already [made](#) [a][aligned] [few][persistence], and I have more in mind, including free and open-source apps, an ultimate charting library for Android, tutorials and samples, and more. But as much as I’d love to give it all away for free, I also need to make a living. Without your support, in a few months I’ll have to return to workforce, meaning I won’t be able to work on my projects anymore. Here’s what you can do to help: * **Check out [Material Cue][mcue-support]**, a keyline app I built with Rhythm. * **Gift me a game from my [wishlist on Steam][steam]** to cheer me up. * **Spread the word:** tell your fellow developers about Rhythm, Material Cue, and my other projects. * **Add me on [Google+][gplus] and/or [Twitter][twitter]** to be the first to know about my upcoming projects and apps. * If you or your company are interested in sponsoring my development efforts, please contact me directly at [actinate@gmail.com](mailto:actinate@gmail.com), and we could arrange something. * Need a hand with making your app more Material? I guess I could allocate some time to freelance work — contact me to discuss the options. Thank you! ## License The library is licensed under [Apache 2.0 License][license], meaning that you can freely use it in any of your projects. ``` Copyright (C) 2016 Actinarium Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ``` Android, Google Play and the Google Play logo are trademarks of Google Inc. --- ¹ — Said so for simplicity. In fact, Rhythm inflates overlay config into [RhythmOverlay](http://actinarium.github.io/Rhythm/javadoc/rhythm/com/actinarium/rhythm/RhythmOverlay.html) objects, which then can be injected into one or many [RhythmDrawables](http://actinarium.github.io/Rhythm/javadoc/rhythm/com/actinarium/rhythm/RhythmDrawable.html), which are, yeah, Drawables. Since it's not 1.0 yet, this may also change. ² — Rhythm Control notification is pretty useless on pre-4.1 (API 15 and below) because of lack of notification actions. The groups and `RhythmFrameLayouts` can still be controlled programmatically though. [mcue]: https://play.google.com/store/apps/details?id=com.actinarium.materialcue&referrer=utm_source%3Dgh-rhythm%26utm_medium%3Dreferral%26utm_term%3Drhythm-readme [mcue-support]: https://play.google.com/store/apps/details?id=com.actinarium.materialcue&referrer=utm_source%3Dgh-rhythm%26utm_medium%3Dreferral%26utm_term%3Drhythm-readme-support [aligned]: https://github.com/Actinarium/Aligned [persistence]: https://plus.google.com/u/0/communities/104144545680241581851 [mdspec]: https://material.google.com/layout/metrics-keylines.html [wiki]: https://github.com/Actinarium/Rhythm/wiki [wiki-config]: https://github.com/Actinarium/Rhythm/wiki/Declarative-configuration [bintray]: https://bintray.com/actinarium/maven/rhythm [license]: https://raw.githubusercontent.com/Actinarium/Rhythm/master/LICENSE [playstore]: https://play.google.com/store/apps/details?id=com.actinarium.rhythm.sample [samplesrc]: https://github.com/Actinarium/Rhythm/tree/master/sample [sampleconfig]: https://github.com/Actinarium/Rhythm/blob/master/sample/src/main/res/raw/overlay_config [javadoc-core]: http://actinarium.github.io/Rhythm/javadoc/rhythm [javadoc-control]: http://actinarium.github.io/Rhythm/javadoc/rhythm-control [gplus]: https://plus.google.com/u/0/+PaulDanyliuk/posts [twitter]: https://twitter.com/actinarium [steam]: http://steamcommunity.com/id/actine/wishlist ================================================ FILE: build.gradle ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // Top-level build file where you can add configuration options common to all sub-projects/modules. project.ext.set("releaseVersion", '0.9.6') project.ext.set("versionCode", 4) buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:2.1.2' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { jcenter() } } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ # # Copyright (C) 2016 Actinarium # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # #Wed Sep 30 00:55:24 EEST 2015 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip ================================================ FILE: gradle.properties ================================================ # # Copyright (C) 2016 Actinarium # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # Project-wide Gradle settings. # IDE (e.g. Android Studio) users: # Gradle settings configured through the IDE *will override* # any settings specified in this file. # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. # Default value: -Xmx10248m -XX:MaxPermSize=256m # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true ================================================ FILE: gradlew ================================================ #!/usr/bin/env bash ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn ( ) { echo "$*" } die ( ) { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; esac # For Cygwin, ensure paths are in UNIX format before anything is touched. if $cygwin ; then [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` fi # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >&- APP_HOME="`pwd -P`" cd "$SAVED" >&- CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules function splitJvmOpts() { JVM_OPTS=("$@") } eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" ================================================ FILE: gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windowz variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* goto execute :4NT_args @rem Get arguments from the 4NT Shell from JP Software set CMD_LINE_ARGS=%$ :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: rhythm/.gitignore ================================================ /build *.iml ================================================ FILE: rhythm/build.gradle ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ apply plugin: 'com.android.library' apply plugin: 'maven' android { compileSdkVersion 23 buildToolsVersion "23.0.3" defaultConfig { minSdkVersion 8 targetSdkVersion 23 versionCode project.versionCode versionName project.releaseVersion } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } lintOptions { disable 'RtlHardcoded','UnusedAttribute' } } configurations { javadocDeps } task androidJavadocs(type: Javadoc) { source = android.sourceSets.main.java.srcDirs classpath += configurations.javadocDeps classpath += project.files(android.bootClasspath) } task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) { classifier = 'javadoc' from androidJavadocs.destinationDir } task androidSourcesJar(type: Jar) { classifier = 'sources' from android.sourceSets.main.java.srcDirs } artifacts { archives androidSourcesJar archives androidJavadocsJar } afterEvaluate { androidJavadocs.classpath += files(android.libraryVariants.collect { variant -> variant.javaCompile.classpath.files }) } uploadArchives { repositories.mavenDeployer { pom.groupId = 'com.actinarium.rhythm' pom.artifactId = 'rhythm' pom.version = project.releaseVersion pom.project { name 'Rhythm Core' description 'Renders grids, guides, and other overlays from declarative configuration into drawables' url 'https://github.com/Actinarium/Rhythm' inceptionYear '2015' licenses { license { name 'The Apache Software License, Version 2.0' url 'http://www.apache.org/licenses/LICENSE-2.0.txt' distribution 'repo' } } organization { name 'Actinarium' url 'http://actinarium.com' } developers { developer { id 'Actine' name 'Paul Danyliuk' url 'https://plus.google.com/u/0/+PaulDanyliuk' roles { role 'architect' role 'developer' } } } scm { url 'https://github.com/Actinarium/Rhythm.git' connection 'scm:git:https://github.com/Actinarium/Rhythm.git' developerConnection 'scm:git:git@github.com:Actinarium/Rhythm.git' } } repository(url: "file://D:/Build") } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:support-annotations:23.3.0' javadocDeps 'com.android.support:support-annotations:23.3.0' } ================================================ FILE: rhythm/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in C:\Program Files (x86)\Android\android-sdk/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} ================================================ FILE: rhythm/src/main/AndroidManifest.xml ================================================ ================================================ FILE: rhythm/src/main/java/com/actinarium/rhythm/AbstractSpecLayerGroup.java ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.actinarium.rhythm; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.view.View; import java.util.ArrayList; import java.util.List; /** * An abstract base class with common functionality for Rhythm overlay layers that have sub-layers * * @author Paul Danyliuk */ public abstract class AbstractSpecLayerGroup implements RhythmSpecLayerParent { protected static final int ESTIMATED_AVG_LAYERS = 8; protected List mLayers; public AbstractSpecLayerGroup() { mLayers = new ArrayList<>(ESTIMATED_AVG_LAYERS); } public AbstractSpecLayerGroup(int initialCapacity) { mLayers = new ArrayList<>(initialCapacity); } @Override public void draw(Canvas canvas, Rect drawableBounds) { for (int i = 0, size = mLayers.size(); i < size; i++) { mLayers.get(i).draw(canvas, drawableBounds); } } /** * Add a spec layer to this group. Note: by default, calling this method DOESN'T trigger redraw. If you are * calling this when overlay is already displayed and want to have changes displayed immediately, you also must call * {@link View#invalidateDrawable(Drawable)} or similar yourself. * * @param layer A Rhythm spec layer * @return this for chaining */ @SuppressWarnings("unchecked") @Override public T addLayer(RhythmSpecLayer layer) { mLayers.add(layer); return (T) this; } /** * Return the number of children in this layer group * @return number of children */ public int size() { return mLayers.size(); } } ================================================ FILE: rhythm/src/main/java/com/actinarium/rhythm/ArgumentsBundle.java ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.actinarium.rhythm; import android.annotation.SuppressLint; import android.support.annotation.ColorInt; import android.support.annotation.IntDef; import android.support.annotation.Nullable; import android.util.DisplayMetrics; import android.view.Gravity; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * An interface that defines an arguments container that will be used by {@link RhythmSpecLayerFactory} implementations * to query spec layer properties when creating new layers. * * @author Paul Danyliuk */ public interface ArgumentsBundle { int UNITS_NULL = -1; int UNITS_NUMBER = 0; int UNITS_PERCENT = 1; int UNITS_PX = 2; int UNITS_DP = 3; int UNITS_SP = 4; int UNITS_PT = 5; int UNITS_IN = 6; int UNITS_MM = 7; /** * Test if there's an argument with given key in the bundle, even if the value is null. * * @param key argument key * @return true if argument is present regardless of value */ boolean hasArgument(String key); /** * Get argument value as a string. * * @param key argument key * @return argument value as a string, or null if the argument has null value or it cannot be retrieved */ String getString(String key); /** * Get argument value as a string with fallback to default value if argument is missing. * * @param key argument key * @param defaultValue fallback value * @return argument value as raw string */ String getString(String key, @Nullable String defaultValue); /** * Get argument value as integer with fallback to default value if argument is missing. * * @param key argument key * @param defaultValue fallback value * @return argument value parsed as integer */ int getInt(String key, int defaultValue); /** * Get argument value as float with fallback to default value if argument is missing. * * @param key argument key * @param defaultValue fallback value * @return argument value parsed as float */ float getFloat(String key, float defaultValue); /** * Get boolean argument. Arguments specified as arg are identical to arg=true. * * @param key argument key * @param defaultValue value if argument is not present * @return argument boolean value */ boolean getBoolean(String key, boolean defaultValue); /** * Get argument value as color integer with fallback to default value if argument is missing. * * @param key argument key * @param defaultValue fallback value * @return argument value parsed as color integer */ @ColorInt int getColor(String key, @ColorInt int defaultValue); /** * Get argument value as a gravity value (a combination of {@link Gravity} constants) with fallback to default value * if argument is missing. * * @param key argument key * @param defaultValue fallback value * @return gravity constant * @see #getEdgeAffinity(String, int) */ int getGravity(String key, int defaultValue); /** * Get argument as an {@link EdgeAffinity} constant, which can be either {@link Gravity#TOP}, {@link Gravity#LEFT}, * {@link Gravity#RIGHT}, or {@link Gravity#BOTTOM}, with fallback to default value if argument is missing or * invalid. * * @param key argument key * @param defaultValue fallback value * @return gravity constant * @see #getGravity(String, int) */ @EdgeAffinity int getEdgeAffinity(String key, @EdgeAffinity int defaultValue); /** * Get the units of a dimension argument. * * @param key argument key * @return dimension argument units, or {@link #UNITS_NULL} if the argument is null or missing */ @DimensionUnits int getDimensionUnits(String key); /** * Get raw numeric value from dimension argument disregarding units and NOT performing any conversion to pixels. * * @param key argument key * @param defaultValue fallback value in pixels * @return dimension argument raw value * @see #getDimensionPixelSize(String, int) * @see #getDimensionPixelOffset(String, int) */ float getDimensionValue(String key, float defaultValue); /** * Get dimension argument value as pixels with possible fallback to default value if argument is missing or invalid. * Unlike {@link #getDimensionPixelSize(String, int)} and {@link #getDimensionPixelSize(String, int)}, this method * doesn't perform any rounding. * * @param key argument key * @param defaultValue fallback value in pixels * @return argument value converted to pixels * @see #getDimensionPixelSize(String, int) * @see #getDimensionPixelOffset(String, int) */ float getDimensionPixelExact(String key, float defaultValue); /** * Get dimension argument value as pixels with possible fallback to default value if argument is missing or invalid. * Unlike {@link #getDimensionPixelSize(String, int)}, this method is expected to round the raw value down to * the closest integer. * * @param key argument key * @param defaultValue fallback value in pixels * @return argument value converted to pixels * @see #getDimensionPixelSize(String, int) * @see #getDimensionPixelExact(String, float) */ int getDimensionPixelOffset(String key, int defaultValue); /** * Get dimension argument value as pixels with possible fallback to default value if argument is missing or invalid. * Unlike {@link #getDimensionPixelOffset(String, int)}}, this method is expected to round the raw value up or * down to the closest integer by common rules, and must ensure the result is at least 1px if original value is * not 0. * * @param key argument key * @param defaultValue fallback value in pixels * @return argument value converted to pixels * @see #getDimensionPixelOffset(String, int) * @see #getDimensionPixelExact(String, float) */ int getDimensionPixelSize(String key, int defaultValue); /** * Get display metrics associated with this arguments bundle. * * @return display metrics object */ DisplayMetrics getDisplayMetrics(); /** * Type definition for dimension argument units */ @Retention(RetentionPolicy.SOURCE) @IntDef({UNITS_NULL, UNITS_NUMBER, UNITS_PERCENT, UNITS_PX, UNITS_DP, UNITS_SP, UNITS_MM, UNITS_PT, UNITS_IN}) public @interface DimensionUnits { } /** * Type definition for screen edge that a keyline or pattern must be attached to. Used by some layers */ @SuppressLint("RtlHardcoded") @Retention(RetentionPolicy.SOURCE) @IntDef({Gravity.TOP, Gravity.BOTTOM, Gravity.LEFT, Gravity.RIGHT, Gravity.NO_GRAVITY}) @interface EdgeAffinity { } } ================================================ FILE: rhythm/src/main/java/com/actinarium/rhythm/MagicVariablesArgumentsBundle.java ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.actinarium.rhythm; import android.support.annotation.NonNull; import android.util.DisplayMetrics; import java.util.Map; /** * An implementation of {@link ArgumentsBundle} that utilizes “magic variables” mechanism to resolve missing * layer arguments: if an argument is not explicitly specified, it tries resolving it from a variable named in a special * pattern @{layer_name}_{arg_name} where dashes are replaced with underscores. * * @author Paul Danyliuk */ public class MagicVariablesArgumentsBundle extends SimpleArgumentsBundle { protected String mLayerNamePrefix; protected Map mVariables; /** * Create a new simple arguments bundle implementation from provided key->value map. * * @param arguments A collection that maps arguments to values. In this implementation both the key and the value * are raw strings, parsed into required data types as requested from the map. The values must be * already provided as parsable literal values — this implementation cannot resolve variables * or calculate expressions.
For performance reasons, this map will be used as is, therefore it * must not be mutated. Furthermore this implementation lacks methods to put new parameters * into the bag. * @param variables A @key->value map containing magic variables to fall back to * @param layerName Layer name as registered in the factory, e.g. grid-lines * @param metrics Display metrics associated with this arguments bundle, required so that dimension values (dp, * sp */ public MagicVariablesArgumentsBundle(@NonNull Map arguments, @NonNull Map variables, String layerName, @NonNull DisplayMetrics metrics) { super(arguments, metrics); mVariables = variables; mLayerNamePrefix = '@' + layerName.replace('-', '_') + '_'; } /** * {@inheritDoc} If the argument is not declared explicitly, will look up if a matching magic variable is present. */ @Override public boolean hasArgument(String key) { return mArguments.containsKey(key) || mVariables.containsKey(mLayerNamePrefix + key.replace('-', '_')); } /** * Resolves argument value from the bundle. If the argument is not present in this bundle's arguments map, tries to * fall back to a variable with a magic name of @{layer_name}_{arg_name} (concatenated layer and * argument names with dashes replaced by underscores). * * @param key key of the argument whose value to resolve * @return string representation of resolved value */ @Override protected String resolveArgument(String key) { String value = mArguments.get(key); if (value == null && !mArguments.containsKey(key)) { value = mVariables.get(mLayerNamePrefix + key.replace('-', '_')); } return value; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } if (!super.equals(o)) { return false; } MagicVariablesArgumentsBundle that = (MagicVariablesArgumentsBundle) o; return mVariables.equals(that.mVariables); } @Override public int hashCode() { int result = super.hashCode(); result = 31 * result + mVariables.hashCode(); return result; } } ================================================ FILE: rhythm/src/main/java/com/actinarium/rhythm/RhythmDrawable.java ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.actinarium.rhythm; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; /** *

Renders the currently assigned {@link RhythmOverlay} and serves as an adapter between Rhythm (which sets the * overlay to draw by this drawable at the moment) and the views where the overlay (grids, keylines etc) must be * applied. You can use it as any other {@link Drawable} from Android SDK, e.g. assign it as background, foreground, * overlay etc, but keep in mind that for different views, separate drawable instances must be created.

For easy * integration with existing layouts, RhythmDrawable can decorate another Drawable — * that is, draw the decorated one below and then the overlay atop. This can be especially useful when decorating the * views that already have backgrounds. Note: as of this version, decoration logic is very limited for the sake * of simplicity, therefore in some cases (e.g. when decorated drawable is a state list or a level list), it may not * respond correctly to state and level changes (e.g. pressing a decorated button won’t highlight it). But since * decoration is mostly intended for ViewGroups, it’s unlikely that this should be an issue under normal use.

*

Normally you shouldn’t extend this class. If you need to perform custom drawing, consider creating a custom {@link * RhythmSpecLayer} implementation instead.

* * @author Paul Danyliuk */ public class RhythmDrawable extends Drawable { protected RhythmOverlay mOverlay; protected Drawable mDecorated; /** * Create a Rhythm drawable for given Rhythm overlay. You can then change the displayed overlay via {@link * #setOverlay(RhythmOverlay)} method. * * @param overlay Rhythm overlay to render into this drawable, can be null. */ public RhythmDrawable(@Nullable RhythmOverlay overlay) { mOverlay = overlay; } @Override public void draw(Canvas canvas) { // Draw decorated drawable if present if (mDecorated != null) { mDecorated.draw(canvas); } // Draw overlay if present if (mOverlay != null) { mOverlay.draw(canvas, getBounds()); } } /** * Get current overlay * * @return Currently active Rhythm overlay, or null if no overlay is set */ public RhythmOverlay getOverlay() { return mOverlay; } /** * Set a {@link RhythmOverlay} for this drawable. Will request redraw of this drawable’s view. * * @param overlay Overlay to draw. Provide null to disable overlay. */ public void setOverlay(@Nullable RhythmOverlay overlay) { mOverlay = overlay; invalidateSelf(); } /** * Get decorated drawable (the one drawn under the overlay) if present * * @return Decorated drawable or null */ public Drawable getDecorated() { return mDecorated; } /** * Set a {@link Drawable} to decorate. Should be used when decorating an existing background or foreground of a view * — this way the original drawable will be preserved and the overlay will be drawn atop. Note: to * function properly, the decorated drawable’s {@link Drawable#setCallback(Callback) callbacks} must be set. Also * see {@link RhythmDrawable the class’ description} for more info on decoration support. * * @param decorated A drawable to draw below the overlay. Set null to remove decorated drawable. */ public void setDecorated(@Nullable Drawable decorated) { mDecorated = decorated; if (mDecorated != null) { mDecorated.setBounds(getBounds()); } invalidateSelf(); } @Override public void setAlpha(int alpha) { // No-op for the overlay for simplicity reasons - propagate to decorated drawable only if (mDecorated != null) { mDecorated.setAlpha(alpha); } } @Override public void setColorFilter(ColorFilter colorFilter) { // No-op for the overlay for simplicity reasons - propagate to decorated drawable only if (mDecorated != null) { mDecorated.setColorFilter(colorFilter); } } @Override public int getOpacity() { final int overlayOpacity = mOverlay == null ? PixelFormat.TRANSPARENT : PixelFormat.TRANSLUCENT; return mDecorated != null ? Drawable.resolveOpacity(mDecorated.getOpacity(), overlayOpacity) : overlayOpacity; } @Override public boolean isStateful() { return mDecorated != null && mDecorated.isStateful(); } @Override public boolean setState(int[] stateSet) { return mDecorated != null && mDecorated.setState(stateSet); } @Override public int[] getState() { return mDecorated != null ? mDecorated.getState() : super.getState(); } @Override public boolean getPadding(@NonNull Rect padding) { if (mDecorated == null) { padding.set(0, 0, 0, 0); return false; } else { return mDecorated.getPadding(padding); } } @Override protected boolean onStateChange(int[] state) { return mDecorated != null && mDecorated.setState(state); } @Override protected boolean onLevelChange(int level) { return mDecorated != null && mDecorated.setLevel(level); } @Override protected void onBoundsChange(Rect bounds) { if (mDecorated != null) { mDecorated.setBounds(bounds); } } } ================================================ FILE: rhythm/src/main/java/com/actinarium/rhythm/RhythmInflationException.java ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.actinarium.rhythm; import android.support.annotation.IntRange; /** * A runtime exception to be thrown when there is an error inflating declarative configuration, usually because of * syntax error or violated argument value constraints. * * @author Paul Danyliuk */ public class RhythmInflationException extends RuntimeException { private int mLineNumber = 0; public RhythmInflationException() { } public RhythmInflationException(String detailMessage) { super(detailMessage); } public RhythmInflationException(String detailMessage, Throwable throwable) { super(detailMessage, throwable); } public RhythmInflationException(Throwable throwable) { super(throwable); } /** * Set the index of the line (0-based) where the error happened. If set, text "Line {x+1}: " will be prepended to * error message * * @param index index of the line where error happened, zero-based * @return this for chaining */ public RhythmInflationException setLineNumber(@IntRange(from = 0) int index) { mLineNumber = index + 1; return this; } @Override public String getMessage() { String message = super.getMessage(); return mLineNumber == 0 ? message : "Line " + mLineNumber + ": " + message; } } ================================================ FILE: rhythm/src/main/java/com/actinarium/rhythm/RhythmOverlay.java ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.actinarium.rhythm; import android.support.annotation.NonNull; /** * Defines a single overlay configuration, i.e. which spec layers (grid lines, keylines etc) must be drawn in the {@link * RhythmDrawable}(s) where this overlay is currently set. Composed of granular {@link RhythmSpecLayer}s, which hold * their own configuration (see descriptions of respectable implementations) and are drawn in the order of adding. * * @author Paul Danyliuk */ public class RhythmOverlay extends AbstractSpecLayerGroup { protected String mTitle; /** * Create a new overlay */ public RhythmOverlay() { super(); } /** * Create a new overlay with initial capacity * * @param initialCapacity anticipated number of child layers */ public RhythmOverlay(int initialCapacity) { super(initialCapacity); } /** * Set the title for this overlay. The title is only displayed in Rhythm control notification — you don't need * it in programmatically created anonymous overlays. * * @param title A convenient title for this overlay. * @return this for chaining */ public RhythmOverlay setTitle(String title) { mTitle = title; return this; } /** * Get overlay title * * @return overlay title of null if the overlay is anonymous */ public String getTitle() { return mTitle; } /** *

Add all layers to this overlay from another. Convenient if you have a common set of layers that you wish to * include in multiple overlays, or want to create an overlay that combines a few others.

Warning: for * simplicity and performance reasons the same layer objects are used, therefore it’s strongly advised that you * don’t mutate them after adding.

* * @param source Existing overlay to add all layers from * @return this for chaining */ public RhythmOverlay addLayersFrom(@NonNull RhythmOverlay source) { mLayers.addAll(source.mLayers); return this; } @Override public String toString() { return mTitle != null ? mTitle : "Untitled overlay@" + Integer.toHexString(hashCode()); } } ================================================ FILE: rhythm/src/main/java/com/actinarium/rhythm/RhythmOverlayInflater.java ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.actinarium.rhythm; import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.RawRes; import android.util.DisplayMetrics; import com.actinarium.rhythm.internal.ReaderUtils; import com.actinarium.rhythm.layer.Columns; import com.actinarium.rhythm.layer.DimensionsLabel; import com.actinarium.rhythm.layer.Fill; import com.actinarium.rhythm.layer.GridLines; import com.actinarium.rhythm.layer.Inset; import com.actinarium.rhythm.layer.Keyline; import com.actinarium.rhythm.layer.RatioKeyline; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** *

A default inflater that creates {@linkplain RhythmOverlay}s from text configuration using registered layer * factories. Supports inflating multiple overlays from configuration files (see the docs) separated by newlines as well as separate overlays; * supports comments and variables, and supporting custom spec layers by allowing to register spec layer * factories.

The provided implementation is a reference one — developers are welcome to subclass this * inflater or any classes of the inflation pipeline to override certain aspects, or implement their own inflation * mechanisms (e.g. different lexers, parse-time validation, transformations, XML/JSON/YAML support etc) entirely from * scratch should they need something different.

* * @author Paul Danyliuk */ public class RhythmOverlayInflater { /** * Initial capacity of {layer type} -> {factory} map. */ private static final int INITIAL_FACTORIES_CAPACITY = 16; /** * A regex to search for arguments in configuration string by a following template: key[=value] */ protected static final Pattern PATTERN_ARGUMENTS = Pattern.compile("([^\\s=]+)(?:=([^\\s]+))?"); /** * A regex to validate and parse variables in configuration string by a following template: @variable=value */ protected static final Pattern PATTERN_VARIABLES = Pattern.compile("(@[\\w]+)=(.*)"); /** * Used internally to indicate that there's no overlay block started at the moment of evaluating current line */ private static final int NOT_STARTED = -1; protected Context mContext; protected DisplayMetrics mDisplayMetrics; protected Map mFactories; protected boolean mAreMagicVariablesEnabled = false; /** *

Create a new instance of default overlay inflater. It comes pre-configured to inflate all bundled {@link * RhythmSpecLayer} types, and you can add custom factories for your custom spec layers.

By default, {@link * GridLines}, {@link Keyline}, {@link RatioKeyline}, and {@link Fill} layer instances are cached and reused for the * same configuration lines — if you don't want this behavior (e.g. if you want to mutate the inflated layers * individually afterwards), create an empty inflater and register the factories yourself like this:

*

     * RhythmOverlayInflater inflater = new RhythmOverlayInflater(context);
     * inflater.registerFactory(GridLines.Factory.LAYER_TYPE, new GridLines.Factory());
     * inflater.registerFactory(Keyline.Factory.LAYER_TYPE, new Keyline.Factory());
     * inflater.registerFactory(RatioKeyline.Factory.LAYER_TYPE, new RatioKeyline.Factory());
     * inflater.registerFactory(Fill.Factory.LAYER_TYPE, new Fill.Factory());
     * inflater.registerFactory(Inset.Factory.LAYER_TYPE, new Inset.Factory());
     * inflater.registerFactory(Columns.Factory.LAYER_TYPE, new Columns.Factory());
     * inflater.registerFactory(DimensionsLabel.Factory.LAYER_TYPE, new DimensionsLabel.Factory());
     * 
* * @param context Context * @return a new overlay inflater instance configured to inflate bundled spec layers * @see #RhythmOverlayInflater(Context) */ public static RhythmOverlayInflater createDefault(Context context) { final RhythmOverlayInflater inflater = new RhythmOverlayInflater(context); // Register bundled spec layers. Wrap keyline, fill, grid, and ratio keyline factories in caching decorators inflater.mFactories.put(GridLines.Factory.LAYER_TYPE, new SimpleCacheFactory<>(new GridLines.Factory())); inflater.mFactories.put(Keyline.Factory.LAYER_TYPE, new SimpleCacheFactory<>(new Keyline.Factory())); inflater.mFactories.put(RatioKeyline.Factory.LAYER_TYPE, new SimpleCacheFactory<>(new RatioKeyline.Factory())); inflater.mFactories.put(Fill.Factory.LAYER_TYPE, new SimpleCacheFactory<>(new Fill.Factory())); inflater.mFactories.put(Inset.Factory.LAYER_TYPE, new Inset.Factory()); inflater.mFactories.put(Columns.Factory.LAYER_TYPE, new Columns.Factory()); inflater.mFactories.put(DimensionsLabel.Factory.LAYER_TYPE, new DimensionsLabel.Factory()); return inflater; } /** * Create a new instance of overlay inflater with no factories registered. Call this constructor only if you need a * blank inflater that you are going to configure from scratch (i.e. by registering all the required factories with * {@link #registerFactory(String, RhythmSpecLayerFactory)}). If you need an inflater with all bundled spec layers * pre-configured, use {@link #createDefault(Context)} instead. * * @param context Context * @see #createDefault(Context) */ public RhythmOverlayInflater(Context context) { mContext = context.getApplicationContext(); mDisplayMetrics = mContext.getResources().getDisplayMetrics(); mFactories = new HashMap<>(INITIAL_FACTORIES_CAPACITY); } /** *

Enable or disable “magic variables” support in this inflater instance. Enabling this will allow to * specify default arguments for spec layers by defining global and local variables named with the following * pattern: @{layer_name}_{arg_name}, where dashes are replaced with underscores.

*

Warning:“magic variables” is an experimental feature and therefore disabled by default. * Normally it shouldn't have significant impact on performance, yet it's advised to only enable it if using magic * variables is really desirable over explicitly setting values to spec layers individually.

* * @param enabled true to enable magic variables support, false to disable it. * @return this for chaining */ public RhythmOverlayInflater setMagicVariablesEnabled(boolean enabled) { mAreMagicVariablesEnabled = enabled; return this; } /** * Register a factory for provided layer type. Use this method to register factories for your custom spec layers or * override default behavior. You can add the same factory for multiple layer types, e.g. for aliasing. * * @param layerType string that identifies a specific spec layer class; the first argument in each config line * @param factory a factory object that will inflate config line into a layer * @return this for chaining */ public RhythmOverlayInflater registerFactory(@NonNull String layerType, @NonNull RhythmSpecLayerFactory factory) { mFactories.put(layerType, factory); return this; } /** * Add an alias for arbitrary layer type. This will make multiple layer type strings map to the same factory. For * custom layers, a slightly more efficient way would be to simply call {@link #registerFactory(String, * RhythmSpecLayerFactory)} multiple times with different strings and the same factory objects to avoid lookups. * * @param existingLayerType layer type string for layer to alias (used for lookup) * @param aliasLayerType layer type string to map to the same factory * @return this for chaining */ public RhythmOverlayInflater addAlias(@NonNull String existingLayerType, @NonNull String aliasLayerType) { RhythmSpecLayerFactory factory = mFactories.get(existingLayerType); if (factory != null) { mFactories.put(aliasLayerType, factory); } else { throw new IllegalArgumentException("No factory registered for type \"" + existingLayerType + "\""); } return this; } /** * Inflate a Rhythm configuration file into a list of {@link RhythmOverlay RhythmOverlays}, which you can then * assign to a group, or make sub-lists of and assign to different groups. * * @param rawResId Raw configuration file with syntax according to the docs * @return A list of inflated Rhythm overlays * @see #inflate(List) */ public List inflate(@RawRes int rawResId) { List lines = ReaderUtils.readLines(mContext, rawResId); return inflate(lines); } /** * Same as {@link #inflate(int)}, but accepts a string for the whole overlay configuration file. This method may * come in handy if you need to bulk inflate several overlays from strings known at runtime. * * @param configString Configuration file passed in whole as a string. Must follow the same syntax rules as the * configuration file, e.g. overlays must be separated by an empty line * @return A list of inflated Rhythm overlays * @see #inflate(int) * @see #inflate(List) */ public List inflate(String configString) { List configStrings = Arrays.asList(configString.split("\\r?\\n")); return inflate(configStrings); } /** *

Same as {@link #inflate(int)}, but accepts the configuration file already split in lines as strings.

*

This method walks over the lines and determines how the config should be split into separate overlays.

* * @param configStrings Configuration file split as separate lines. Must follow the same syntax rules as the * configuration file, that is, no null strings, and overlays being separated by * an empty line * @return A list of inflated Rhythm overlays * @see #inflate(int) * @see #inflate(String) */ public List inflate(List configStrings) { List overlays = new ArrayList<>(); Map globalVars = new HashMap<>(); final int len = configStrings.size(); int overlayStart = NOT_STARTED; // Line index int i = 0; // First let's read global variables, which must be placed in the beginning of the file for (; i < len; i++) { final String line = configStrings.get(i); if (isEmptyOrComment(line.trim())) { continue; } if (line.charAt(0) == '@') { // Variable declaration. Let's check and parse it Matcher matcher = PATTERN_VARIABLES.matcher(line); if (!matcher.matches()) { // Oops, bad variable syntax throw new RhythmInflationException( "Malformed variable declaration.\nExpected syntax is @name=value where name may contain only letters, digits, and/or underscores." ).setLineNumber(i); } // Otherwise we're fine String name = matcher.group(1); String value = resolveVariableInternal(globalVars, matcher.group(2), i); globalVars.put(name, value); } else { // Found a non-variable-declaration, non-empty line break; } } // Now read the remaining lines, separating blocks by empty lines, and inflate the blocks as we go for (; i < len; i++) { final String line = configStrings.get(i); if (line.trim().length() == 0) { // We encountered an empty line, meaning this is the end of the previous block if the latter is present if (overlayStart != NOT_STARTED) { // There was a block, so now it's terminated and we should inflate it. final RhythmOverlay previousBlock = inflateOverlayInternal(configStrings.subList(overlayStart, i), globalVars, overlayStart); overlays.add(previousBlock); overlayStart = NOT_STARTED; } } else if (overlayStart == NOT_STARTED && !isEmptyOrComment(line)) { // It's a title, a var, or a layer, which starts a new block overlayStart = i; } } // If we reached the end of the file, and have a block started, inflate it if (overlayStart != NOT_STARTED) { final RhythmOverlay previousBlock = inflateOverlayInternal(configStrings.subList(overlayStart, len), globalVars, overlayStart); overlays.add(previousBlock); } return overlays; } /** * Inflate a single overlay from overlay configuration string according to the syntax spec. * * @param configString layer configuration string, following the syntax rules * @return inflated Rhythm overlay */ @SuppressWarnings("unchecked") public RhythmOverlay inflateOverlay(String configString) { List configStrings = Arrays.asList(configString.split("\\r?\\n")); return inflateOverlayInternal(configStrings, Collections.EMPTY_MAP, 0); } /** * Inflate a single overlay from overlay configuration string according to the syntax spec. * * @param configString layer configuration string, following the syntax rules * @param vars the @key->value map of the values that can be referenced within this overlay (see the * docs) * @return inflated Rhythm overlay */ public RhythmOverlay inflateOverlay(String configString, @NonNull Map vars) { List configStrings = Arrays.asList(configString.split("\\r?\\n")); return inflateOverlayInternal(configStrings, vars, 0); } /** * Inflate a single overlay from overlay configuration already presented as separate lines. * * @param configStrings layer configuration split in lines * @return inflated Rhythm overlay */ @SuppressWarnings("unchecked") public RhythmOverlay inflateOverlay(List configStrings) { return inflateOverlayInternal(configStrings, Collections.EMPTY_MAP, 0); } /** * Inflate a single overlay from overlay configuration already presented as separate lines. * * @param configStrings layer configuration split in lines * @param vars the @key->value map of the values that can be referenced within this overlay (see the * docs) * @return inflated Rhythm overlay */ public RhythmOverlay inflateOverlay(List configStrings, @NonNull Map vars) { return inflateOverlayInternal(configStrings, vars, 0); } /** * Internal method for inflating an overlay from separate config lines, with provided global variables map, and * possibly as a part of an overlay config file. * * @param configStrings layer configuration split in lines * @param globalVars map of global variables * @param offset index of the line where this overlay starts in the context of an outer config. Pass 0 if * inflating this overlay on its own * @return inflated Rhythm overlay */ protected RhythmOverlay inflateOverlayInternal(List configStrings, @NonNull Map globalVars, int offset) { // initialize stacks for parents and indents. Since there's no adequate stack implementations out there for API 8+, make own. // Assume there rarely will be more than 4-deep hierarchy int size = 4; int[] indents = new int[size]; RhythmSpecLayerParent[] parents = new RhythmSpecLayerParent[size]; int headIndex = 0; // at the bottom of the stack we have the new RhythmOverlay object final RhythmOverlay overlay = new RhythmOverlay(); parents[0] = overlay; indents[0] = -1; // At first assume there are no local overrides, so reusing global vars map for now Map localVars = globalVars; boolean hasLocalVars = false; // Read line by line, evaluate line types, parse and nest for (int i = 0, lines = configStrings.size(); i < lines; i++) { String line = configStrings.get(i); if (isEmptyOrComment(line.trim())) { // Empty or comment line, no-op (btw there should be no empty lines here if inflating the whole file) continue; } final int lineNumber = i + offset; if (line.charAt(0) == '@') { // This is a local variable. And all variables must be declared before any overlay lines. if (overlay.size() != 0) { throw new RhythmInflationException( "Unexpected variable declaration.\nVariables must be declared before spec layers." ).setLineNumber(lineNumber); } // If it's the first local var, copy the global vars map where we'll be adding/overwriting values if (!hasLocalVars) { localVars = new HashMap<>(globalVars); hasLocalVars = true; } // Let's check and parse Matcher matcher = PATTERN_VARIABLES.matcher(line); if (matcher.matches()) { String name = matcher.group(1); String value = resolveVariableInternal(localVars, matcher.group(2), lineNumber); localVars.put(name, value); } else { // Oops, bad variable syntax throw new RhythmInflationException( "Malformed variable declaration: \"" + line + "\".\nExpected syntax is @name=value where name may contain only letters, digits, and/or underscores." ).setLineNumber(lineNumber); } } else if (line.charAt(0) == '#') { // Looks like a title. A title should be the first non-empty line, and there should be no multiple titles per block if (overlay.getTitle() != null || hasLocalVars || overlay.size() != 0) { throw new RhythmInflationException( "Unexpected overlay title.\nThere can be only one title per overlay, and it must be the first line. Did you forget an empty newline before starting a new overlay?" ).setLineNumber(lineNumber); } // Otherwise OK, we probably have a title String title = line.substring(1).trim(); if (title.length() != 0) { overlay.setTitle(title); } } else { // Otherwise assume the line is a spec layer, try parsing and inflating it as a separate layer LayerConfig config = parseConfigInternal(line, localVars, lineNumber); // If indent is <= indent of parent layer, then go up the hierarchy. Won't underflow b/c indents[0] is -1 while (config.getIndent() <= indents[headIndex]) { headIndex--; // we could clean up the stacks but there's really no need } RhythmSpecLayer thisLayer = inflateLayerInternal(config, lineNumber); parents[headIndex].addLayer(thisLayer); // if this is a layer group, add it to the stack if (thisLayer instanceof RhythmSpecLayerParent) { headIndex++; // if arrays run out of space, grow it twice (a-la ArrayList) if (headIndex >= size) { int newSize = size * 2; int[] newIndents = new int[newSize]; RhythmSpecLayerParent[] newParents = new RhythmSpecLayerParent[newSize]; System.arraycopy(indents, 0, newIndents, 0, size); System.arraycopy(parents, 0, newParents, 0, size); indents = newIndents; parents = newParents; size = newSize; } parents[headIndex] = (RhythmSpecLayerParent) thisLayer; indents[headIndex] = config.getIndent(); } } } // If there are only variables and nothing else, seems like the user tried to declare global variables between overlay blocks if (hasLocalVars && overlay.size() == 0 && overlay.getTitle() == null) { throw new RhythmInflationException( "Unexpected variable declaration.\nGlobal variables must be declared before all overlay blocks." ).setLineNumber(offset); } return overlay; } /** * Inflate an individual layer from raw configuration string and optional variables * * @param configString configuration string to parse and feed to layer's factory * @param vars map of @key->value mappings used to resolve argument references (e.g. * @primary=#FF0000 to use in color=@primary). Cannot be * null — pass {@link Collections#EMPTY_MAP} if there are no variables. * @return inflated layer */ public RhythmSpecLayer inflateLayer(String configString, @NonNull Map vars) { return inflateLayerInternal(parseConfigInternal(configString, vars, 0), 0); } /** * Inflate an individual layer from already parsed layer configuration. * * @param config parsed layer configuration * @param lineNumber number of the configuration line we're inflating. Required for error reporting. * @return inflated layer */ protected RhythmSpecLayer inflateLayerInternal(LayerConfig config, int lineNumber) { RhythmSpecLayerFactory factory = mFactories.get(config.getLayerType()); if (factory == null) { Object[] knownLayers = mFactories.keySet().toArray(); throw new RhythmInflationException( "Unknown layer type \"" + config.getLayerType() + "\".\nAvailable types are: " + Arrays.toString(knownLayers) ).setLineNumber(lineNumber); } try { return factory.getForArguments(config.getArgumentsBundle()); } catch (RhythmInflationException e) { // Set line number and rethrow throw e.setLineNumber(lineNumber); } catch (Exception e) { // Catch all other exceptions (e.g. IllegalArgument etc) and wrap'em in RhythmInflationException throw new RhythmInflationException( "Error inflating layer: " + e.getMessage(), e ).setLineNumber(lineNumber); } } /** * Parses a line with single layer configuration. Resolves referenced variables into values for consistency. * Developers can override this method to perform parsing differently or return a different implementation of {@link * LayerConfig} or enclosed {@link ArgumentsBundle}. * * @param configString configuration string, indented with spaces if required, starting with layer title and * containing args or key=value pairs * @param vars map of @key->value mappings used to resolve argument references (e.g. * @primary=#FF0000 to use in color=@primary) * @param lineNumber Line number to report in case of error * @return layer config object with layer configuration and metadata */ protected LayerConfig parseConfigInternal(String configString, @NonNull Map vars, int lineNumber) { // We can parse everything using pattern matcher. The first match would be our layer name Matcher matcher = PATTERN_ARGUMENTS.matcher(configString); if (!matcher.find()) { // The whole layer line is malformed throw new RhythmInflationException( "Malformed spec layer declaration.\nExpected format is = =..." ).setLineNumber(lineNumber); } final String specLayerType = matcher.group(); final int spaces = matcher.start(); final Map arguments = new HashMap<>(); while (matcher.find()) { String key = matcher.group(1); // Since we're already resolving variables in inflater and not lazily upon reading from arguments, // let's be consistent and do the same for individual args as well. String value = resolveVariableInternal(vars, matcher.group(2), lineNumber); arguments.put(key, value); } // Experimental magic variables support integrated here: ArgumentsBundle argumentsBundle = mAreMagicVariablesEnabled ? new MagicVariablesArgumentsBundle(arguments, vars, specLayerType, mDisplayMetrics) : new SimpleArgumentsBundle(arguments, mDisplayMetrics); return new LayerConfig(specLayerType, spaces, argumentsBundle); } /** * Resolve variable value: if it's a reference to another variable (i.e. starts with '@'), try resolving its value, * otherwise return as is. There's no need to resolve references recursively, as all previously declared variables * already have their values resolved. * * @param vars Variables map, as resolved at the moment * @param value Value that's either a reference to resolve or a concrete value * @param lineNumber Line number to report in case of error * @return variable value */ protected String resolveVariableInternal(@NonNull Map vars, String value, int lineNumber) { if (value != null && value.length() != 0 && value.charAt(0) == '@') { if (vars.containsKey(value)) { value = vars.get(value); } else { throw new RhythmInflationException( "Cannot resolve variable " + value ).setLineNumber(lineNumber); } } return value; } /** * Utility method that determines whether the line is empty or a comment one (starts with //) and thus * should be ignored. * * @param line line to test, should be pre-trimmed * @return true if empty or comment */ public static boolean isEmptyOrComment(String line) { return line.length() == 0 || (line.charAt(0) == '/' && line.length() >= 2 && line.charAt(1) == '/'); } /** * A spec layer descriptor holding arguments and metadata, used internally by {@link RhythmOverlayInflater} to carry * values needed to inflate individual layers and their hierarchies. * * @author Paul Danyliuk */ public static class LayerConfig { protected String mLayerType; protected int mIndent; protected ArgumentsBundle mArgumentsBundle; /** * Create layer config object for layer of given type, with known indent, and with pre-filled arguments bag * * @param layerType spec layer type, used for appropriate factory lookup * @param indent number of leading spaces in the config line, used to resolve layer hierarchy * @param argumentsBundle an object describing parsed layer configuration (arguments and values) */ public LayerConfig(@NonNull String layerType, int indent, @NonNull ArgumentsBundle argumentsBundle) { mLayerType = layerType; mIndent = indent; mArgumentsBundle = argumentsBundle; } /** * Get the name of {@link RhythmSpecLayer spec layer} to inflate with these arguments * * @return spec layer type */ public String getLayerType() { return mLayerType; } /** * Get the number of spaces this config line was indented with. Used internally to resolve grouping * * @return number of spaces */ public int getIndent() { return mIndent; } /** * Get the configuration of this layer presented by an {@link ArgumentsBundle} object * * @return layer configuration bundle */ public ArgumentsBundle getArgumentsBundle() { return mArgumentsBundle; } } } ================================================ FILE: rhythm/src/main/java/com/actinarium/rhythm/RhythmSpecLayer.java ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.actinarium.rhythm; import android.graphics.Canvas; import android.graphics.Rect; /** *

Spec layer is a descriptor of a granular piece of overlay (e.g. a single line, a repeating line etc), which both * holds the configuration of its appearance (hence the spec) and is also capable of drawing itself onto the provided * canvas (hence the layer). Unlike Drawables, where separate instances are required each time they are used, spec layer * instances are created per configuration and can be reused across many {@link RhythmDrawable}s (views, * overlays).

You can create custom spec layers by implementing this interface.

*/ public interface RhythmSpecLayer { /** * Draw itself to the provided canvas within provided bounds according to internal configuration (if any) * * @param canvas Canvas for the layer to draw itself to * @param drawableBounds Bounds where this layer should draw itself. Since these are the bounds of a {@link * RhythmDrawable} connected to the view, they are usually the same as the view’s bounds, so * you can use this parameter to get the view’s dimensions should you need them. */ void draw(Canvas canvas, Rect drawableBounds); } ================================================ FILE: rhythm/src/main/java/com/actinarium/rhythm/RhythmSpecLayerFactory.java ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.actinarium.rhythm; /** *

Interface for a factory that can instantiate a {@link RhythmSpecLayer} implementation from provided {@link * ArgumentsBundle}. These factories are used by {@link RhythmOverlayInflater} to inflate declarative config into * respective overlays. If you make a custom spec layer, you should also create a corresponding * RhythmSpecLayerFactory and register it within {@link RhythmOverlayInflater#registerFactory(String, * RhythmSpecLayerFactory)} method.

Concrete factories may implement some sort of caching and provide the same * {@link RhythmSpecLayer} instances for equal {@linkplain ArgumentsBundle ArgumentsBundles} if they can be reused, but * it's not mandatory. Furthermore it’s developer’s responsibility to not mutate the layer if the latter is reused in * multiple overlays.

* * @author Paul Danyliuk */ public interface RhythmSpecLayerFactory { /** * Create and configure a spec layer from provided arguments, or get previously created one from cache if it can be * safely reused. * * @param argsBundle container with arguments for this layer * @return configured layer */ T getForArguments(ArgumentsBundle argsBundle); } ================================================ FILE: rhythm/src/main/java/com/actinarium/rhythm/RhythmSpecLayerParent.java ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.actinarium.rhythm; /** * An object that can contain other spec layers. Extends {@link RhythmSpecLayer}, therefore must know how to draw itself * (usually just iterate over children and draw them in order within provided bounds). * * @author Paul Danyliuk */ public interface RhythmSpecLayerParent extends RhythmSpecLayer { /** * Add layer to this parent. Since this is mostly intended for initial configuration, it is NOT mandatory that * implementations of this method trigger redraw. * * @param layer Layer to add * @return this for chaining */ RhythmSpecLayerParent addLayer(RhythmSpecLayer layer); } ================================================ FILE: rhythm/src/main/java/com/actinarium/rhythm/SimpleArgumentsBundle.java ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.actinarium.rhythm; import android.annotation.SuppressLint; import android.graphics.Color; import android.support.annotation.ColorInt; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.DisplayMetrics; import android.util.TypedValue; import android.view.Gravity; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * A basic implementation of {@link ArgumentsBundle}, which stores all arguments simply as a String->String key-value * map and parses them into required types when accessed by respective getter methods (meaning it also fails lazily). * Does not cache parsing results, so if the same arguments are requested multiple times, it may be a good idea to query * them once and store the result in a variable. * * @author Paul Danyliuk */ public class SimpleArgumentsBundle implements ArgumentsBundle { protected Map mArguments; protected DisplayMetrics mMetrics; protected static Pattern DIMEN_VALUE_PATTERN = Pattern.compile("^-?\\d*\\.?\\d+"); /** * Create a new simple arguments bundle implementation from provided key->value map. * * @param arguments A collection that maps arguments to values. In this implementation both the key and the value * are raw strings, parsed into required data types as requested from the map. The values must be * already provided as parsable literal values — this implementation cannot resolve variables * or calculate expressions.
For performance reasons, this map will be used as is, therefore it * must not be mutated. Furthermore this implementation lacks methods to put new parameters * into the bag. * @param metrics Display metrics associated with this arguments bundle, required so that dimension values (dp, sp * etc) can be properly resolved. */ public SimpleArgumentsBundle(@NonNull Map arguments, @NonNull DisplayMetrics metrics) { mArguments = arguments; mMetrics = metrics; } /** * {@inheritDoc} */ @Override public DisplayMetrics getDisplayMetrics() { return mMetrics; } @Override public boolean hasArgument(String key) { return mArguments.containsKey(key); } /** * Resolves argument value from the bundle. This implementation simply returns the raw string as put in the argument * map by the inflater. Used internally by all getXxx() methods — subclasses should override this * method if additional processing is required (e.g. lazy variable dereference, expression evaluation etc). * * @param key key of the argument whose value to resolve * @return string representation of the value */ protected String resolveArgument(String key) { return mArguments.get(key); } /** * {@inheritDoc} For simple arguments bundle, will return the raw string as put in the arguments map by the * inflater. If you need to change how raw value is resolved, override {@link #resolveArgument(String)} */ @Override public String getString(String key) { return resolveArgument(key); } /** * {@inheritDoc} Will return the raw string as put in the arguments map by the inflater. */ @Override public String getString(String key, @Nullable String defaultValue) { String rawValue = resolveArgument(key); return rawValue != null ? rawValue : defaultValue; } @Override public int getInt(String key, int defaultValue) { String rawValue = resolveArgument(key); return rawValue != null ? Integer.parseInt(rawValue) : defaultValue; } @Override public float getFloat(String key, float defaultValue) { String rawValue = resolveArgument(key); return rawValue != null ? Float.parseFloat(rawValue) : defaultValue; } @Override public boolean getBoolean(String key, boolean defaultValue) { String rawValue = resolveArgument(key); if (rawValue != null) { return Boolean.parseBoolean(rawValue); } else { return mArguments.containsKey(key) || defaultValue; } } @Override @ColorInt public int getColor(String key, @ColorInt int defaultValue) { String rawValue = resolveArgument(key); return rawValue != null ? Color.parseColor(rawValue) : defaultValue; } /** * {@inheritDoc} Does a quick and rough parsing of the raw string for containing constant words like * top or center_vertical */ @Override @SuppressLint("RtlHardcoded") public int getGravity(String key, int defaultValue) { String gravityArg = resolveArgument(key); if (gravityArg == null) { return defaultValue; } else if (gravityArg.equals("center")) { return Gravity.CENTER; } else if (gravityArg.equals("fill")) { return Gravity.FILL; } else { // supported options int gravity = 0; if (gravityArg.contains("top")) { gravity |= Gravity.TOP; } if (gravityArg.contains("bottom")) { gravity |= Gravity.BOTTOM; } if (gravityArg.contains("center_vertical")) { gravity |= Gravity.CENTER_VERTICAL; } if (gravityArg.contains("fill_vertical")) { gravity |= Gravity.FILL_VERTICAL; } if (gravityArg.contains("left")) { gravity |= Gravity.LEFT; } if (gravityArg.contains("right")) { gravity |= Gravity.RIGHT; } if (gravityArg.contains("center_horizontal")) { gravity |= Gravity.CENTER_HORIZONTAL; } if (gravityArg.contains("fill_horizontal")) { gravity |= Gravity.FILL_HORIZONTAL; } return gravity; } } @Override @SuppressLint("RtlHardcoded") @EdgeAffinity public int getEdgeAffinity(String key, @EdgeAffinity int defaultValue) { String gravityArg = resolveArgument(key); if ("top".equals(gravityArg)) { return Gravity.TOP; } else if ("left".equals(gravityArg)) { return Gravity.LEFT; } else if ("right".equals(gravityArg)) { return Gravity.RIGHT; } else if ("bottom".equals(gravityArg)) { return Gravity.BOTTOM; } else { return defaultValue; } } /** * {@inheritDoc} Note: this is a very crude implementation relying only on the check of trailing string * characters (i.e. whether the string ends with "dp", "px", "%" etc). However, for development-time library and * assuming that developers are not their own enemies, that should be fine. */ @Override @DimensionUnits public int getDimensionUnits(String key) { String value = resolveArgument(key); if (value == null) { return UNITS_NULL; } else if (value.endsWith("dp") || value.endsWith("dip")) { return UNITS_DP; } else if (value.endsWith("px")) { return UNITS_PX; } else if (value.endsWith("%")) { return UNITS_PERCENT; } else if (value.endsWith("sp")) { return UNITS_SP; } else if (value.endsWith("pt")) { return UNITS_PT; } else if (value.endsWith("in")) { return UNITS_IN; } else if (value.endsWith("mm")) { return UNITS_MM; } else { // assume raw number, try to parse as float return UNITS_NUMBER; } } /** * {@inheritDoc} * * @see #getDimensionPixelRaw(float, int, DisplayMetrics) */ @Override public float getDimensionValue(String key, float defaultValue) { String value = resolveArgument(key); if (value == null) { return defaultValue; } Matcher matcher = DIMEN_VALUE_PATTERN.matcher(value); if (matcher.find()) { return Float.parseFloat(matcher.group()); } else { return defaultValue; } } /** * {@inheritDoc} Note: this method requires that {@link DisplayMetrics} object is injected in this config. * * @see #getDimensionPixelRaw(float, int, DisplayMetrics) */ @Override public float getDimensionPixelExact(String key, float defaultValue) { @DimensionUnits int units = getDimensionUnits(key); if (units == UNITS_NULL) { return defaultValue; } float rawValue = getDimensionValue(key, defaultValue); return getDimensionPixelRaw(rawValue, units, mMetrics); } /** * {@inheritDoc} Note: this method requires that {@link DisplayMetrics} object is injected in this config. * * @see #getDimensionPixelRaw(float, int, DisplayMetrics) */ @Override public int getDimensionPixelOffset(String key, int defaultValue) { @DimensionUnits int units = getDimensionUnits(key); if (units == UNITS_NULL) { return defaultValue; } float rawValue = getDimensionValue(key, defaultValue); return (int) getDimensionPixelRaw(rawValue, units, mMetrics); } /** * {@inheritDoc} Note: this method requires that {@link DisplayMetrics} object is injected in this config. * * @see #getDimensionPixelRaw(float, int, DisplayMetrics) */ @Override public int getDimensionPixelSize(String key, int defaultValue) { @DimensionUnits int units = getDimensionUnits(key); float rawValue = getDimensionValue(key, defaultValue); float result = getDimensionPixelRaw(rawValue, units, mMetrics); final int res = (int) (result + 0.5f); if (res != 0) { return res; } if (rawValue == 0) { return 0; } if (rawValue > 0) { return 1; } return defaultValue; } /** * Convert complex dimension value of provided units into pixels. * * @param value raw dimension value, e.g. 24f * @param units dimension units, one of {@link #UNITS_PX}, {@link #UNITS_DP}, {@link #UNITS_SP}, {@link * #UNITS_PT}, {@link #UNITS_IN}, {@link #UNITS_MM}, {@link #UNITS_NUMBER}, {@link #UNITS_NULL}, or * {@link #UNITS_PERCENT} * @param metrics display metrics to convert complex dimension types that depend on density (dp, sp etc) into * pixels, can be null if type is one of {@link #UNITS_PX}, {@link #UNITS_PERCENT}, {@link * #UNITS_NUMBER}, or {@link #UNITS_NULL} * @return dimension value in pixels */ public static float getDimensionPixelRaw(float value, @DimensionUnits int units, DisplayMetrics metrics) { switch (units) { case UNITS_DP: return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, metrics); case UNITS_PX: case UNITS_PERCENT: case UNITS_NUMBER: case UNITS_NULL: return value; case UNITS_SP: return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, value, metrics); case UNITS_PT: return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PT, value, metrics); case UNITS_IN: return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_IN, value, metrics); case UNITS_MM: return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, value, metrics); default: return 0; } } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } return mArguments.equals(((SimpleArgumentsBundle) o).mArguments); } @Override public int hashCode() { return mArguments.hashCode(); } } ================================================ FILE: rhythm/src/main/java/com/actinarium/rhythm/SimpleCacheFactory.java ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.actinarium.rhythm; import java.util.HashMap; import java.util.Map; /** * A decorator for a spec layer factory that performs simple caching of previously inflated layers * * @author Paul Danyliuk */ public class SimpleCacheFactory implements RhythmSpecLayerFactory { private RhythmSpecLayerFactory mDecoratedFactory; private Map mCache; public SimpleCacheFactory(RhythmSpecLayerFactory decoratedFactory) { mDecoratedFactory = decoratedFactory; mCache = new HashMap<>(); } /** * Returns layer for this configuration from cache, or creates a new one via decorated factory if not found in * cache * * @param argsBundle container with arguments for this layer * @return layer for this configuration, either new or from cache */ @Override public T getForArguments(ArgumentsBundle argsBundle) { T layer = mCache.get(argsBundle); if (layer != null) { return layer; } // if cache miss, inflate the new one layer = mDecoratedFactory.getForArguments(argsBundle); mCache.put(argsBundle, layer); return layer; } } ================================================ FILE: rhythm/src/main/java/com/actinarium/rhythm/internal/ReaderUtils.java ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.actinarium.rhythm.internal; import android.content.Context; import android.support.annotation.RawRes; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; /** * Utility class that contains methods for reading data from {@linkplain InputStream input streams} and raw resources * * @author Paul Danyliuk */ public final class ReaderUtils { private ReaderUtils() {} /** * Reads lines from input stream. Doesn’t close the stream — you have to close it yourself. * * @param inputStream Input stream * @return List of lines read from the stream * @see #readLines(Context, int) */ public static List readLines(InputStream inputStream) { final BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, Charset.defaultCharset())); ArrayList readLines = new ArrayList<>(); String line; try { while ((line = reader.readLine()) != null) { readLines.add(line); } } catch (IOException e) { throw new RuntimeIOException(e); } return readLines; } /** * Reads lines from given raw resource. Opens the file, reads the lines with {@link #readLines(InputStream)} and * closes the stream afterwards. * * @param context Context to retrieve the resource * @param rawResId Raw resource ID, which is a name of a file in /res/raw folder without extension * @return List of lines read from the file */ public static List readLines(Context context, @RawRes int rawResId) { final InputStream inputStream = context.getResources().openRawResource(rawResId); try { return readLines(inputStream); } finally { try { inputStream.close(); } catch (IOException e) { // Fail quietly } } } } ================================================ FILE: rhythm/src/main/java/com/actinarium/rhythm/internal/RuntimeIOException.java ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.actinarium.rhythm.internal; import java.io.IOException; /** * A runtime counterpart of {@link IOException} thrown when the library cannot operate on provided data stream. * * @author Paul Danyliuk */ public class RuntimeIOException extends RuntimeException { public RuntimeIOException() { super(); } public RuntimeIOException(String detailMessage) { super(detailMessage); } public RuntimeIOException(Throwable throwable) { super(throwable); } public RuntimeIOException(String detailMessage, Throwable throwable) { super(detailMessage, throwable); } } ================================================ FILE: rhythm/src/main/java/com/actinarium/rhythm/layer/Columns.java ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.actinarium.rhythm.layer; import android.graphics.Canvas; import android.graphics.Rect; import android.support.annotation.IntRange; import com.actinarium.rhythm.AbstractSpecLayerGroup; import com.actinarium.rhythm.RhythmInflationException; import com.actinarium.rhythm.ArgumentsBundle; import com.actinarium.rhythm.RhythmSpecLayerFactory; /** * A layer that divides provided bounds into arbitrary number of equally wide columns and then draws nested layers in * each column. * * @author Paul Danyliuk */ public class Columns extends AbstractSpecLayerGroup { @IntRange(from = 1) protected int mColumnCount; private Rect mTemp = new Rect(); /** * Create spec layer that will evenly divide current bounds in given number of columns and then draw all child * layers in each * * @param columnCount number of columns, must be a positive integer */ public Columns(@IntRange(from = 1) int columnCount) { super(); mColumnCount = columnCount; } /** * Create spec layer that will evenly divide current bounds in given number of columns and then draw all child * layers in each * * @param columnCount number of columns, must be a positive integer * @param initialCapacity anticipated number of child layers */ public Columns(@IntRange(from = 1) int columnCount, int initialCapacity) { super(initialCapacity); mColumnCount = columnCount; } /** *

Create spec layer that will evenly divide current bounds in given number of columns and then draw all child * layers in each.

This is a minimum constructor for the factory — only paints and reusable objects are * initialized. Developers extending this class are responsible for setting all fields to proper argument * values.

*/ protected Columns() { super(); } /** * Set the number of columns * * @param columnCount number of columns, must be a positive integer * @return this for chaining */ public Columns setColumnCount(@IntRange(from = 1) int columnCount) { mColumnCount = columnCount; return this; } @Override public void draw(Canvas canvas, Rect drawableBounds) { mTemp.set(drawableBounds); final int left = drawableBounds.left; final float width = drawableBounds.width(); for (int i = 1; i <= mColumnCount; i++) { // Always adding rounded i/count fraction of width to the fixed left to ensure symmetry // and that the bounds don't overflow overall width mTemp.right = left + (int) Math.floor(width * i / mColumnCount + 0.5f); // Draw all children into the column super.draw(canvas, mTemp); // Offset the temporary rect mTemp.left = mTemp.right; } } /** * A default factory that creates new {@link Columns} layers from config lines according to the docs */ public static class Factory implements RhythmSpecLayerFactory { public static final String LAYER_TYPE = "columns"; public static final String ARG_COUNT = "count"; @Override public Columns getForArguments(ArgumentsBundle argsBundle) { Columns columns = new Columns(); columns.mColumnCount = argsBundle.getInt(ARG_COUNT, 0); if (columns.mColumnCount <= 0) { throw new RhythmInflationException( "Error in columns config: 'count' argument is mandatory and must be greater than 0" ); } return columns; } } } ================================================ FILE: rhythm/src/main/java/com/actinarium/rhythm/layer/DimensionsLabel.java ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.actinarium.rhythm.layer; import android.annotation.SuppressLint; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.support.annotation.ColorInt; import android.support.annotation.FloatRange; import android.text.Layout; import android.text.StaticLayout; import android.text.TextPaint; import android.util.DisplayMetrics; import android.view.Gravity; import com.actinarium.rhythm.RhythmSpecLayer; import com.actinarium.rhythm.ArgumentsBundle; import com.actinarium.rhythm.RhythmSpecLayerFactory; import java.text.DecimalFormat; /** * A layer that draws a small box with dimensions of the current view. Inspect the dimensions of your views at glance. * noticing the issues asap. By default, the box is placed in the bottom right corner, but you can change its gravity * with {@link #setGravity(int)}. Experimental at the moment, meaning its behavior, appearance, and parameters may * change. * * @author Paul Danyliuk */ public class DimensionsLabel implements RhythmSpecLayer { public static final int DEFAULT_BACKGROUND = 0x80000000; public static final int DEFAULT_TEXT_COLOR = 0xA0FFFFFF; public static final float DEFAULT_SCALE_FACTOR = 1f; public static final int DEFAULT_TEXT_SIZE = 12; // px // Pretty print chars public static final char ONE_HALF = '\u00bd'; public static final char ONE_FOURTH = '\u00bc'; public static final char THREE_FOURTHS = '\u00be'; public static final char ONE_THIRD = '\u2153'; public static final char TWO_THIRDS = '\u2154'; public static final char MULTIPLY = '\u00d7'; protected static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.##"); @FloatRange(from = 0.0, fromInclusive = false) protected float mScaleFactor; @SuppressLint("RtlHardcoded") protected int mGravity = Gravity.BOTTOM | Gravity.RIGHT; protected Paint mBackgroundPaint; protected TextPaint mTextPaint; private Rect mTemp = new Rect(); public DimensionsLabel() { mBackgroundPaint = new Paint(); mBackgroundPaint.setStyle(Paint.Style.FILL); mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); mScaleFactor = DEFAULT_SCALE_FACTOR; mBackgroundPaint.setColor(DEFAULT_BACKGROUND); mTextPaint.setTextSize(DEFAULT_TEXT_SIZE); } /** * Set a scale factor that will be applied to width and height of provided bounds * * @param scaleFactor Scale factor to divide pixels by. Provide {@link DisplayMetrics#density} here to display * dimensions as dips, {@link DisplayMetrics#scaledDensity} to display them as sp, * or {@link #DEFAULT_SCALE_FACTOR} (1f) to get pixels. * @return this for chaining */ public DimensionsLabel setScaleFactor(@FloatRange(from = 0.0, fromInclusive = false) float scaleFactor) { mScaleFactor = scaleFactor; return this; } /** * Set label gravity. Default is bottom right. * * @param gravity Desired gravity. Can be combinations, e.g. {@link Gravity#BOTTOM} | {@link * Gravity#LEFT} * @return this for chaining */ public DimensionsLabel setGravity(int gravity) { mGravity = gravity; return this; } /** * Set label background color * * @param color Label background color, in #AARRGGBB format as usual * @return this for chaining */ public DimensionsLabel setBackgroundColor(@ColorInt int color) { mBackgroundPaint.setColor(color); return this; } /** * Set the color of the label text itself * * @param color Label text color, in #AARRGGBB format as usual * @return this for chaining */ public DimensionsLabel setTextColor(@ColorInt int color) { mTextPaint.setColor(color); return this; } /** * Set text size * * @param size Text size, in pixels * @return this for chaining */ public DimensionsLabel setTextSize(@FloatRange(from = 0.0, fromInclusive = false) float size) { mTextPaint.setTextSize(size); return this; } @Override public void draw(Canvas canvas, Rect drawableBounds) { final int intWidth = drawableBounds.width(); // Make the label text based on width, height, and scale factor String text = prettyPrintDips(intWidth, mScaleFactor) + ' ' + MULTIPLY + ' ' + prettyPrintDips(drawableBounds.height(), mScaleFactor); // Use StaticLayout, which will calculate text dimensions nicely, then position the box using Gravity.apply() // (although that's one instantiation per draw call...) // This is what happens if you're obsessed with perfection like me StaticLayout layout = new StaticLayout(text, mTextPaint, intWidth, Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false); Gravity.apply(mGravity, (int) (layout.getLineMax(0) + 0.5), layout.getHeight(), drawableBounds, mTemp); // Draw background canvas.drawRect(mTemp, mBackgroundPaint); // We have to translate the canvas ourselves, since layout can only draw itself at (0, 0) canvas.save(); canvas.translate(mTemp.left, mTemp.top); layout.draw(canvas); canvas.restore(); } /** * Sophisticated conversion of pixels to dips with the use of vulgar fractions (to save screen space) * * @param px Pixels to convert to dips * @param scaleFactor Scale factor, should be equal to {@link DisplayMetrics#density} for px to dp conversion * @return String formatted with vulgar fraction if needed and possible */ public static String prettyPrintDips(int px, float scaleFactor) { String dip; if (scaleFactor == 1f) { dip = String.valueOf(px); } else if (scaleFactor == 2f) { dip = String.valueOf(px / 2); if (px % 2 == 1) { dip += ONE_HALF; } } else if (scaleFactor == 3f) { dip = String.valueOf(px / 3); if (px % 3 == 1) { dip += ONE_THIRD; } else if (px % 3 == 2) { dip += TWO_THIRDS; } } else if (scaleFactor == 4f) { dip = String.valueOf(px / 4); if (px % 4 == 1) { dip += ONE_FOURTH; } else if (px % 4 == 2) { dip += ONE_HALF; } else if (px % 4 == 3) { dip += THREE_FOURTHS; } } else { // Very hard to determine exactly, so falling back to decimals dip = DECIMAL_FORMAT.format(px / scaleFactor); } return dip; } /** * A default factory that creates new {@link DimensionsLabel} layers from config lines according to the docs */ public static class Factory implements RhythmSpecLayerFactory { public static final String LAYER_TYPE = "dimensions-label"; public static final String ARG_GRAVITY = "gravity"; public static final String ARG_COLOR = "color"; public static final String ARG_TEXT_COLOR = "text-color"; public static final String ARG_TEXT_SIZE = "text-size"; @SuppressLint("RtlHardcoded") @Override public DimensionsLabel getForArguments(ArgumentsBundle argsBundle) { DimensionsLabel label = new DimensionsLabel(); final float density = argsBundle.getDisplayMetrics().density; label.mScaleFactor = density; label.mGravity = argsBundle.getGravity(ARG_GRAVITY, Gravity.BOTTOM | Gravity.RIGHT); label.mBackgroundPaint.setColor(argsBundle.getColor(ARG_COLOR, DEFAULT_BACKGROUND)); label.mTextPaint.setColor(argsBundle.getColor(ARG_TEXT_COLOR, DEFAULT_TEXT_COLOR)); // todo: it shouldn't be the factory's concern to pre-multiply default text size by density - think of how to handle this gracefully label.mTextPaint.setTextSize(argsBundle.getDimensionPixelExact(ARG_TEXT_SIZE, DEFAULT_TEXT_SIZE * density)); return label; } } } ================================================ FILE: rhythm/src/main/java/com/actinarium/rhythm/layer/Fill.java ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.actinarium.rhythm.layer; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.support.annotation.ColorInt; import com.actinarium.rhythm.RhythmSpecLayer; import com.actinarium.rhythm.ArgumentsBundle; import com.actinarium.rhythm.RhythmSpecLayerFactory; /** * A layer that fills all provided area with solid color. You will usually want to use it inside {@link Inset} to draw * rectangles (margins, gutters etc). * * @author Paul Danyliuk */ public class Fill implements RhythmSpecLayer { public static final int DEFAULT_FILL_COLOR = 0x400091EA; protected Paint mPaint; /** * Create a layer that fills current bounds with solid color */ public Fill() { mPaint = new Paint(); mPaint.setStyle(Paint.Style.FILL); mPaint.setColor(DEFAULT_FILL_COLOR); } /** * Set fill color * * @param color Fill color, in #AARRGGBB format as usual * @return this for chaining */ public Fill setColor(@ColorInt int color) { mPaint.setColor(color); return this; } @Override public void draw(Canvas canvas, Rect drawableBounds) { canvas.drawRect(drawableBounds, mPaint); } /** * A default factory that creates new {@link Fill} layers from config lines according to the docs */ public static class Factory implements RhythmSpecLayerFactory { public static final String LAYER_TYPE = "fill"; public static final String ARG_COLOR = "color"; @Override public Fill getForArguments(ArgumentsBundle argsBundle) { Fill fill = new Fill(); fill.setColor(argsBundle.getColor(ARG_COLOR, DEFAULT_FILL_COLOR)); return fill; } } } ================================================ FILE: rhythm/src/main/java/com/actinarium/rhythm/layer/GridLines.java ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.actinarium.rhythm.layer; import android.annotation.SuppressLint; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.support.annotation.ColorInt; import android.support.annotation.FloatRange; import android.support.annotation.IntRange; import android.view.Gravity; import com.actinarium.rhythm.RhythmSpecLayer; import com.actinarium.rhythm.RhythmInflationException; import com.actinarium.rhythm.ArgumentsBundle; import com.actinarium.rhythm.RhythmSpecLayerFactory; /** * A spec layer for horizontal or vertical grid lines (not both at once!), repeating at a fixed step. Horizontal * grid can float either to the top or the bottom edge of the views, whereas vertical grid can float to the left or the * right. You can (and should) combine multiple grid line layers to form regular grids, or you may use them alone for * baseline grids and incremental keylines. Note: RTL properties are not supported, you only have left and * right at your disposal. */ public class GridLines implements RhythmSpecLayer { public static final int DEFAULT_GRID_COLOR = 0x800091EA; /** * Default grid line thickness (1px) */ public static final int DEFAULT_THICKNESS = 1; // px @FloatRange(from = 0f, fromInclusive = false) protected float mStep; @IntRange(from = 1) protected int mThickness = DEFAULT_THICKNESS; protected int mLimit = Integer.MAX_VALUE; protected int mOffset; @ArgumentsBundle.EdgeAffinity protected int mEdgeAffinity; protected Paint mPaint; /** * Create a layer that draws horizontal or vertical grid lines. Unless offset is applied, horizontal lines are * always drawn below the delimited pixel row, and vertical lines are always drawn to the right of the * delimited column: e.g. if a child view is fully aligned to the grid on all edges, top and bottom grid lines will * overdraw the view, whereas bottom and right grid lines will touch the view. * * @param edgeAffinity Controls grid alignment and orientation. {@link Gravity#TOP} and {@link * Gravity#BOTTOM} mean horizontal lines, and {@link Gravity#LEFT} and {@link Gravity#RIGHT} * mean vertical lines, and the difference between those is from what edge of the view the steps * are counted. A good example where this can be useful is having a left-aligned and a * right-aligned layer on the left and the right half of the view when its width is not an exact * multiple of the step. * @param step Grid step, in pixels. Allows for float values to properly accommodate devices with non-round * dip-to-pixel ratio (1.5x on hdpi, 2.5x on Nexus 5X etc) */ public GridLines(@ArgumentsBundle.EdgeAffinity int edgeAffinity, @FloatRange(from = 0f, fromInclusive = false) float step) { this(); mStep = step; mEdgeAffinity = edgeAffinity; mPaint.setColor(DEFAULT_GRID_COLOR); } /** *

Create a spec layer that displays dimensions label.

This is a minimum constructor for the factory * — only paints and reusable objects are initialized. Developers extending this class are responsible for * setting all fields to proper argument values.

*/ protected GridLines() { mPaint = new Paint(); mPaint.setStyle(Paint.Style.FILL); } /** * Set the step of grid lines * * @param step Grid step, in pixels. Allows for float values to properly accommodate devices with non-round * dip-to-pixel ratio (1.5x on hdpi, 2.5x on Nexus 5X etc) * @return this for chaining */ public GridLines setStep(@FloatRange(from = 0f, fromInclusive = false) float step) { mStep = step; return this; } /** * Set edge affinity of the grid * * @param edgeAffinity Controls grid alignment and orientation. Use {@link Gravity#TOP} or {@link * Gravity#BOTTOM} for horizontal lines counting from top or bottom, and {@link Gravity#LEFT} or * {@link Gravity#RIGHT} for vertical lines cointing from left or right edge of the screen * respectively. * @return this for chaining */ public GridLines setEdgeAffinity(@ArgumentsBundle.EdgeAffinity int edgeAffinity) { mEdgeAffinity = edgeAffinity; return this; } /** * Set grid line color * * @param color Grid line color, in #AARRGGBB format as usual * @return this for chaining */ public GridLines setColor(@ColorInt int color) { mPaint.setColor(color); return this; } /** * Set grid line thickness * * @param thickness Grid line thickness, in pixels * @return this for chaining */ public GridLines setThickness(@IntRange(from = 1) int thickness) { mThickness = thickness; return this; } /** * Set the maximum number of steps to outline, respecting layer’s gravity (i.e. if gravity is set to {@link * Gravity#BOTTOM} and the limit is 4, this layer will draw four lines enclosing 4 cells. Default is no limit. * * @param limit Number of lines to draw. Setting zero or less means no limit. * @return this for chaining */ public GridLines setLimit(int limit) { mLimit = limit > 0 ? limit : Integer.MAX_VALUE; return this; } /** * Set additional grid offset. Might be useful if you need to tweak the position of the grid just a few pixels up or * down, or prevent overdraw when combining a few interleaving grids (e.g. to add a 4dp baseline grid to a 8dp * regular grid you only need to draw each second baseline, which is done with a 8dp step and a 4dp offset). * * @param offset Grid offset in pixels. Regardless of gravity, positive offset means right/down, negative means * left/up * @return this for chaining */ public GridLines setOffset(int offset) { mOffset = offset; return this; } @SuppressLint("RtlHardcoded") @Override public void draw(Canvas canvas, Rect drawableBounds) { // Depending on gravity the orientation, the order of drawing, and the starting point are different if (mEdgeAffinity == Gravity.TOP) { final float top = drawableBounds.top + mOffset + 0.5f; for (int i = 0; i <= mLimit; i++) { int y = (int) (top + mStep * i); if (y >= drawableBounds.bottom) { return; } canvas.drawRect(drawableBounds.left, y, drawableBounds.right, y + mThickness, mPaint); } } else if (mEdgeAffinity == Gravity.BOTTOM) { final float bottom = drawableBounds.bottom + mOffset + 0.5f; for (int i = 0; i <= mLimit; i++) { int y = (int) (bottom - mStep * i); if (y < drawableBounds.top) { return; } canvas.drawRect(drawableBounds.left, y, drawableBounds.right, y + mThickness, mPaint); } } else if (mEdgeAffinity == Gravity.LEFT) { final float left = drawableBounds.left + mOffset + 0.5f; for (int i = 0; i <= mLimit; i++) { int x = (int) (left + mStep * i); if (x >= drawableBounds.right) { return; } canvas.drawRect(x, drawableBounds.top, x + mThickness, drawableBounds.bottom, mPaint); } } else if (mEdgeAffinity == Gravity.RIGHT) { final float right = drawableBounds.right + mOffset + 0.5f; for (int i = 0; i <= mLimit; i++) { int x = (int) (right - mStep * i); if (x < drawableBounds.left) { return; } canvas.drawRect(x, drawableBounds.top, x + mThickness, drawableBounds.bottom, mPaint); } } } /** * A default factory that creates new {@link GridLines} layers from config lines according to the docs */ public static class Factory implements RhythmSpecLayerFactory { public static final String LAYER_TYPE = "grid-lines"; public static final String ARG_EDGE = "from"; public static final String ARG_STEP = "step"; public static final String ARG_COLOR = "color"; public static final String ARG_THICKNESS = "thickness"; public static final String ARG_LIMIT = "limit"; public static final String ARG_OFFSET = "offset"; @Override public GridLines getForArguments(ArgumentsBundle argsBundle) { GridLines gridLines = new GridLines(); gridLines.mEdgeAffinity = argsBundle.getEdgeAffinity(ARG_EDGE, Gravity.NO_GRAVITY); if (gridLines.mEdgeAffinity == Gravity.NO_GRAVITY) { throw new RhythmInflationException( "Error in grid-lines config: 'from' argument is mandatory and must be either 'left', 'right', 'top', 'bottom'" ); } final float step = argsBundle.getDimensionPixelExact(ARG_STEP, 0f); if (step <= 0) { throw new RhythmInflationException( "Error in grid-lines config: 'step' argument is mandatory and must be greater than 0" ); } gridLines.mStep = step; gridLines.mPaint.setColor(argsBundle.getColor(ARG_COLOR, DEFAULT_GRID_COLOR)); gridLines.mThickness = argsBundle.getDimensionPixelSize(ARG_THICKNESS, DEFAULT_THICKNESS); gridLines.setLimit(argsBundle.getInt(ARG_LIMIT, Integer.MAX_VALUE)); gridLines.mOffset = argsBundle.getDimensionPixelOffset(ARG_OFFSET, 0); return gridLines; } } } ================================================ FILE: rhythm/src/main/java/com/actinarium/rhythm/layer/Inset.java ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.actinarium.rhythm.layer; import android.graphics.Canvas; import android.graphics.Rect; import android.support.annotation.IntDef; import com.actinarium.rhythm.AbstractSpecLayerGroup; import com.actinarium.rhythm.ArgumentsBundle; import com.actinarium.rhythm.RhythmSpecLayerFactory; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * A group that clips and/or insets its child layers. Allows setting left, right, top, and bottom insets, positive or * negative, as well as width and height, all in either absolute dimensions or as percent of parent. Setting width or * height takes precedence over setting insets in the following way: if width (height) and right (bottom) are set but * left (top) is not, the block floats to the right (bottom), otherwise it stays on the left (top). * * @author Paul Danyliuk */ public class Inset extends AbstractSpecLayerGroup { /** * Inset the bounds and clip the overlay. Default behavior */ public static final int MODE_DEFAULT = 0; /** * Inset the bounds but don't clip drawing to the overlay. Best used for margins */ public static final int MODE_NO_CLIP = 1; /** * Clip the group according to inset rect but keep the coordinates. Best for clipping an absolutely positioned * overlay */ public static final int MODE_CLIP_ONLY = 2; /** * Dimension specified in pixels */ public static final boolean UNITS_PX = false; /** * Dimension specified in percent of container */ public static final boolean UNITS_PERCENT = true; @Mode protected int mMode = MODE_DEFAULT; // Insets protected boolean mIsLeftPercent; protected boolean mIsTopPercent; protected boolean mIsRightPercent; protected boolean mIsBottomPercent; protected int mLeft; protected int mTop; protected int mRight; protected int mBottom; // Dimensions - override insets protected boolean mIsWidthPercent; protected boolean mIsHeightPercent; protected int mWidth; protected int mHeight; // Flags for set values - determine how insets are calculated protected boolean mIsLeftSet; protected boolean mIsRightSet; protected boolean mIsTopSet; protected boolean mIsBottomSet; protected boolean mIsWidthSet; protected boolean mIsHeightSet; // Reusable resulting rect protected Rect mInsetRect = new Rect(); /** * Create a layer group that clips and/or insets its child layers */ public Inset() { super(); } /** * Create a layer group that clips and/or insets its child layers * * @param initialCapacity anticipated number of child layers */ public Inset(int initialCapacity) { super(initialCapacity); } /** * Set inset mode — whether the group should also clip the children, translate the coordinates, or both * * @param mode one of {@link #MODE_NO_CLIP}, {@link #MODE_CLIP_ONLY}, or {@link #MODE_DEFAULT} * @return this for chaining * @see #MODE_DEFAULT * @see #MODE_NO_CLIP * @see #MODE_CLIP_ONLY */ public Inset setMode(@Mode int mode) { mMode = mode; return this; } /** * Set top inset * * @param value pixels or percent * @param isPercent true if value is in percent, false if in pixels * @return this for chaining */ public Inset setTop(int value, boolean isPercent) { mIsTopSet = true; mTop = value; mIsTopPercent = isPercent; return this; } /** * Set bottom inset * * @param value pixels or percent * @param isPercent true if value is in percent, false if in pixels * @return this for chaining */ public Inset setBottom(int value, boolean isPercent) { mIsBottomSet = true; mBottom = value; mIsBottomPercent = isPercent; return this; } /** * Set left inset * * @param value pixels or percent * @param isPercent true if value is in percent, false if in pixels * @return this for chaining */ public Inset setLeft(int value, boolean isPercent) { mIsLeftSet = true; mLeft = value; mIsLeftPercent = isPercent; return this; } /** * Set right inset * * @param value pixels or percent * @param isPercent true if value is in percent, false if in pixels * @return this for chaining */ public Inset setRight(int value, boolean isPercent) { mIsRightSet = true; mRight = value; mIsRightPercent = isPercent; return this; } /** * Set width. If both width, left inset, and right inset are set, right inset is ignored * * @param value pixels or percent * @param isPercent true if value is in percent, false if in pixels * @return this for chaining */ public Inset setWidth(int value, boolean isPercent) { mIsWidthSet = true; mWidth = value; mIsWidthPercent = isPercent; return this; } /** * Set height. If both height, top inset, and bottom inset are set, bottom inset is ignored * * @param value pixels or percent * @param isPercent true if value is in percent, false if in pixels * @return this for chaining */ public Inset setHeight(int value, boolean isPercent) { mIsHeightSet = true; mHeight = value; mIsHeightPercent = isPercent; return this; } @Override public void draw(Canvas canvas, Rect drawableBounds) { // Assume this is a) not called very often, and b) is a fast operation anyway recalculateInsetRect(drawableBounds); final int state = canvas.save(); if (mMode != MODE_NO_CLIP) { canvas.clipRect(mInsetRect); } if (mMode == MODE_CLIP_ONLY) { // Draw sub-layers within original bounds super.draw(canvas, drawableBounds); } else { // Draw sub-layers within new bounds super.draw(canvas, mInsetRect); } canvas.restoreToCount(state); } /** * Update the inset bounds based on provided outer bounds and this layer's state * * @param outerBounds Outer bounds provided to this inset layer to modify */ protected void recalculateInsetRect(Rect outerBounds) { final int parentWidth = outerBounds.width(); final int parentHeight = outerBounds.height(); if (!mIsWidthSet) { // No width - inset based on left and right. Assume those are set, otherwise those are 0 anyway mInsetRect.left = outerBounds.left + (mIsLeftPercent ? parentWidth * mLeft / 100 : mLeft); mInsetRect.right = outerBounds.right - (mIsRightPercent ? parentWidth * mRight / 100 : mRight); } else if (mIsRightSet && !mIsLeftSet) { // Width and right are set, left not set but calculated from width mInsetRect.right = outerBounds.right - (mIsRightPercent ? parentWidth * mRight / 100 : mRight); mInsetRect.left = mInsetRect.right - (mIsWidthPercent ? parentWidth * mWidth / 100 : mWidth); } else { // If right not set, or all three are set, right is ignored and calculated as left + width mInsetRect.left = outerBounds.left + (mIsLeftPercent ? parentWidth * mLeft / 100 : mLeft); mInsetRect.right = mInsetRect.left + (mIsWidthPercent ? parentWidth * mWidth / 100 : mWidth); } if (!mIsHeightSet) { // No height - inset based on top and bottom. Assume those are set, otherwise those are 0 anyway mInsetRect.top = outerBounds.top + (mIsTopPercent ? parentHeight * mTop / 100 : mTop); mInsetRect.bottom = outerBounds.bottom - (mIsBottomPercent ? parentHeight * mBottom / 100 : mBottom); } else if (mIsBottomSet && !mIsTopSet) { // Height and bottom are set, top not set but calculated from height mInsetRect.bottom = outerBounds.bottom - (mIsBottomPercent ? parentHeight * mBottom / 100 : mBottom); mInsetRect.top = mInsetRect.bottom - (mIsHeightPercent ? parentHeight * mHeight / 100 : mHeight); } else { // If bottom not set, or all three are set, bottom is ignored and calculated as top + height mInsetRect.top = outerBounds.top + (mIsTopPercent ? parentHeight * mTop / 100 : mTop); mInsetRect.bottom = mInsetRect.top + (mIsHeightPercent ? parentHeight * mHeight / 100 : mHeight); } } /** * A default factory that creates new {@link Inset} layers from config lines according to the docs */ public static class Factory implements RhythmSpecLayerFactory { public static final String LAYER_TYPE = "inset"; public static final String ARG_NO_CLIP = "no-clip"; public static final String ARG_CLIP_ONLY = "clip-only"; public static final String ARG_TOP = "top"; public static final String ARG_BOTTOM = "bottom"; public static final String ARG_LEFT = "left"; public static final String ARG_RIGHT = "right"; public static final String ARG_WIDTH = "width"; public static final String ARG_HEIGHT = "height"; @Override public Inset getForArguments(ArgumentsBundle argsBundle) { Inset inset = new Inset(); if (argsBundle.hasArgument(ARG_NO_CLIP)) { inset.mMode = MODE_NO_CLIP; } else if (argsBundle.hasArgument(ARG_CLIP_ONLY)) { inset.mMode = MODE_CLIP_ONLY; } else { inset.mMode = MODE_DEFAULT; } if (argsBundle.hasArgument(ARG_TOP)) { boolean isPercent = argsBundle.getDimensionUnits(ARG_TOP) == ArgumentsBundle.UNITS_PERCENT; int value = argsBundle.getDimensionPixelOffset(ARG_TOP, 0); inset.setTop(value, isPercent); } if (argsBundle.hasArgument(ARG_BOTTOM)) { boolean isPercent = argsBundle.getDimensionUnits(ARG_BOTTOM) == ArgumentsBundle.UNITS_PERCENT; int value = argsBundle.getDimensionPixelOffset(ARG_BOTTOM, 0); inset.setBottom(value, isPercent); } if (argsBundle.hasArgument(ARG_LEFT)) { boolean isPercent = argsBundle.getDimensionUnits(ARG_LEFT) == ArgumentsBundle.UNITS_PERCENT; int value = argsBundle.getDimensionPixelOffset(ARG_LEFT, 0); inset.setLeft(value, isPercent); } if (argsBundle.hasArgument(ARG_RIGHT)) { boolean isPercent = argsBundle.getDimensionUnits(ARG_RIGHT) == ArgumentsBundle.UNITS_PERCENT; int value = argsBundle.getDimensionPixelOffset(ARG_RIGHT, 0); inset.setRight(value, isPercent); } if (argsBundle.hasArgument(ARG_WIDTH)) { boolean isPercent = argsBundle.getDimensionUnits(ARG_WIDTH) == ArgumentsBundle.UNITS_PERCENT; int value = argsBundle.getDimensionPixelSize(ARG_WIDTH, 0); inset.setWidth(value, isPercent); } if (argsBundle.hasArgument(ARG_HEIGHT)) { boolean isPercent = argsBundle.getDimensionUnits(ARG_HEIGHT) == ArgumentsBundle.UNITS_PERCENT; int value = argsBundle.getDimensionPixelSize(ARG_HEIGHT, 0); inset.setHeight(value, isPercent); } return inset; } } /** * Type definition for inset group type */ @Retention(RetentionPolicy.SOURCE) @IntDef({MODE_DEFAULT, MODE_NO_CLIP, MODE_CLIP_ONLY}) public @interface Mode { } } ================================================ FILE: rhythm/src/main/java/com/actinarium/rhythm/layer/Keyline.java ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.actinarium.rhythm.layer; import android.annotation.SuppressLint; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.support.annotation.ColorInt; import android.support.annotation.IntRange; import android.view.Gravity; import com.actinarium.rhythm.RhythmSpecLayer; import com.actinarium.rhythm.RhythmInflationException; import com.actinarium.rhythm.ArgumentsBundle; import com.actinarium.rhythm.RhythmSpecLayerFactory; /** * A layer that draws a horizontal or vertical full-bleed keyline at the specified distance from the specified edge of a * view. Can be used to draw “thin” keylines, as well as thick highlights (e.g. margins in avatar list * view), although it's recommended to use {@link Fill} inside an {@link Inset} for the latter. The keyline is drawn * towards the specified edge by default (i.e. touching aligned child views), but this can be tweaked using {@link * #setAlignOutside(boolean)} method. * * @author Paul Danyliuk */ public class Keyline implements RhythmSpecLayer { public static final int DEFAULT_KEYLINE_COLOR = 0x60F50057; /** * Default keyline thickness (2px) */ public static final int DEFAULT_THICKNESS = 2; // px public static final boolean ALIGN_INSIDE = false; public static final boolean ALIGN_OUTSIDE = true; @ArgumentsBundle.EdgeAffinity protected int mEdgeAffinity; protected int mDistance; @IntRange(from = 1) protected int mThickness; protected boolean mAlignOutside; protected Paint mPaint; /** * Create a layer that draws a horizontal or vertical keyline at a specified distance from required edge * * @param edgeAffinity Defines the edge of the view this keyline must be anchored to. Values ({@link Gravity#LEFT} * and {@link Gravity#RIGHT}) will result in a vertical keyline, and values ({@link Gravity#TOP} * and {@link Gravity#BOTTOM}) will result in a horizontal keyline. * @param distance Distance of this keyline from the specified edge, in pixels * @see #setAlignOutside(boolean) */ public Keyline(@ArgumentsBundle.EdgeAffinity int edgeAffinity, int distance) { this(); mEdgeAffinity = edgeAffinity; mDistance = distance; mThickness = DEFAULT_THICKNESS; mPaint.setColor(DEFAULT_KEYLINE_COLOR); } /** *

Create a layer that draws a horizontal or vertical keyline at a specified distance from required edge.

*

This is a minimum constructor for the factory — only paints and reusable objects are initialized. * Developers extending this class are responsible for setting all fields to proper argument values.

*/ protected Keyline() { mPaint = new Paint(); mPaint.setStyle(Paint.Style.FILL); } /** * Set the distance of the keyline from specified edge * * @param distance Distance of this keyline from the specified edge, in pixels * @return this for chaining */ public Keyline setDistance(int distance) { mDistance = distance; return this; } /** * Set edge affinity of the keyline * * @param edgeAffinity Defines the edge of the view this keyline must be anchored to. Values ({@link Gravity#LEFT} * and {@link Gravity#RIGHT}) will result in a vertical keyline, and values ({@link Gravity#TOP} * and {@link Gravity#BOTTOM}) will result in a horizontal keyline. * @return this for chaining */ public Keyline setEdgeAffinity(int edgeAffinity) { mEdgeAffinity = edgeAffinity; return this; } /** * Set keyline color * * @param color Grid line color, in #AARRGGBB format as usual * @return this for chaining */ public Keyline setColor(@ColorInt int color) { mPaint.setColor(color); return this; } /** * Set keyline thickness * * @param thickness Keyline thickness, in pixels. For keylines keep thickness around a few pixels, whereas for * highlights feel free to use as many dips as required. * @return this for chaining * @see #setAlignOutside(boolean) */ public Keyline setThickness(@IntRange(from = 1) int thickness) { mThickness = thickness; return this; } /** * Set keyline alignment. By default, the keyline is drawn towards the specified edge, i.e. if edge affinity is * BOTTOM, distance is 24px and thickness is 6px, the keyline will appear as a horizontal rectangle starting at the * 18th and ending at the 23rd pixel row from the bottom. You can use this method to override that behavior and make * the keyline face outwards (24th to 29th pixel rows in aforementioned example). * * @param alignOutside either false ({@link #ALIGN_INSIDE}, default) for the keyline to extend towards * the edge defined by edge affinity, or true ({@link #ALIGN_OUTSIDE}) to extend * away from the edge * @return this for chaining */ public Keyline setAlignOutside(boolean alignOutside) { mAlignOutside = alignOutside; return this; } @SuppressLint("RtlHardcoded") @Override public void draw(Canvas canvas, Rect drawableBounds) { if (mEdgeAffinity == Gravity.LEFT) { // Vertical line at offset points from the left final int rightX = drawableBounds.left + mDistance + (mAlignOutside ? mThickness : 0); canvas.drawRect(rightX - mThickness, drawableBounds.top, rightX, drawableBounds.bottom, mPaint); } else if (mEdgeAffinity == Gravity.RIGHT) { // Vertical line at offset points from the right final int leftX = drawableBounds.right - mDistance - (mAlignOutside ? mThickness : 0); canvas.drawRect(leftX, drawableBounds.top, leftX + mThickness, drawableBounds.bottom, mPaint); } else if (mEdgeAffinity == Gravity.TOP) { // Horizontal line at offset points from the top final int bottomY = drawableBounds.top + mDistance + (mAlignOutside ? mThickness : 0); canvas.drawRect(drawableBounds.left, bottomY - mThickness, drawableBounds.right, bottomY, mPaint); } else if (mEdgeAffinity == Gravity.BOTTOM) { // Horizontal line at offset points from the top final int topY = drawableBounds.bottom - mDistance - (mAlignOutside ? mThickness : 0); canvas.drawRect(drawableBounds.left, topY, drawableBounds.right, topY + mThickness, mPaint); } } /** * A default factory that creates new {@link Keyline} layers from config lines according to the docs */ public static class Factory implements RhythmSpecLayerFactory { public static final String LAYER_TYPE = "keyline"; public static final String ARG_EDGE = "from"; public static final String ARG_DISTANCE = "distance"; public static final String ARG_COLOR = "color"; public static final String ARG_THICKNESS = "thickness"; public static final String ARG_OUTSIDE = "outside"; @Override public Keyline getForArguments(ArgumentsBundle argsBundle) { Keyline keyline = new Keyline(); keyline.mEdgeAffinity = argsBundle.getEdgeAffinity(ARG_EDGE, Gravity.NO_GRAVITY); if (keyline.mEdgeAffinity == Gravity.NO_GRAVITY) { throw new RhythmInflationException( "Error in keyline config: 'from' argument is mandatory and must be either 'left', 'right', 'top', 'bottom'" ); } if (!argsBundle.hasArgument(ARG_DISTANCE)) { throw new RhythmInflationException( "Error in keyline config: 'distance' argument is mandatory and must be a dimension value (e.g. 'distance=16dp')" ); } keyline.mDistance = argsBundle.getDimensionPixelOffset(ARG_DISTANCE, 0); keyline.mPaint.setColor(argsBundle.getColor(ARG_COLOR, DEFAULT_KEYLINE_COLOR)); keyline.mThickness = argsBundle.getDimensionPixelSize(ARG_THICKNESS, DEFAULT_THICKNESS); keyline.mAlignOutside = argsBundle.getBoolean(ARG_OUTSIDE, ALIGN_INSIDE); return keyline; } } } ================================================ FILE: rhythm/src/main/java/com/actinarium/rhythm/layer/RatioKeyline.java ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.actinarium.rhythm.layer; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; import android.support.annotation.ColorInt; import android.support.annotation.IntRange; import android.support.annotation.Nullable; import android.text.Layout; import android.text.StaticLayout; import android.text.TextPaint; import android.util.DisplayMetrics; import android.util.TypedValue; import android.view.Gravity; import com.actinarium.rhythm.ArgumentsBundle; import com.actinarium.rhythm.RhythmInflationException; import com.actinarium.rhythm.RhythmSpecLayer; import com.actinarium.rhythm.RhythmSpecLayerFactory; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * A single horizontal keyline whose distance from the top is calculated from the width of current bounds and given * aspect ratio. Displays the label in the bottom right corner of the enclosed rectangle. Experimental at the * moment, meaning its behavior, appearance, and parameters may change. As of now, the keyline label is always 12dp * high, 24dp wide, has 10dp font that shrinks if not fitting, and is always within the enclosed rectangle, i.e. * overdrawing its bottom edge. * * @author Paul Danyliuk */ public class RatioKeyline implements RhythmSpecLayer { public static final int DEFAULT_FILL_COLOR = 0xB03F51B5; public static final int DEFAULT_TEXT_COLOR = 0xC0FFFFFF; public static final int DEFAULT_THICKNESS = 2; // px // todo: all defaults must be in px, not depending on density public static final int DEFAULT_TEXT_SIZE = 10; // dp protected static final int DEFAULT_LABEL_HEIGHT = 12; // dp @IntRange(from = 0) protected int mRatioX; @IntRange(from = 0) protected int mRatioY; @IntRange(from = 1) protected int mThickness; protected String mText; protected Paint mBackgroundPaint; protected TextPaint mTextPaint; protected Rect mTempRect; protected Path mLabelPath; // Text adjustment protected int mLabelRectWidth; protected int mLabelHeight; protected int mLabelSideWidth; public RatioKeyline(@IntRange(from = 0) int ratioX, @IntRange(from = 0) int ratioY, DisplayMetrics metrics) { this(metrics); mRatioX = ratioX; mRatioY = ratioY; mBackgroundPaint.setColor(DEFAULT_FILL_COLOR); mTextPaint.setColor(DEFAULT_TEXT_COLOR); } protected RatioKeyline(DisplayMetrics metrics) { mTempRect = new Rect(); mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mBackgroundPaint.setStyle(Paint.Style.FILL); mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); mLabelPath = new Path(); // Hard-coded defaults mTextPaint.setTextSize((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_TEXT_SIZE, metrics)); // Make default keyline path mLabelHeight = (int) ((DEFAULT_LABEL_HEIGHT) * metrics.density); mLabelRectWidth = mLabelHeight * 2; mLabelSideWidth = mLabelHeight * 3 / 4; mLabelPath.rLineTo(0, -mLabelHeight); mLabelPath.rLineTo(-mLabelRectWidth, 0); mLabelPath.rLineTo(-mLabelSideWidth, mLabelHeight); mLabelPath.close(); } /** * Set ratio of the box this keyline should define, in form of two terms * * @param ratioX antecedent, horizontal component of the ratio (e.g. 16 in 16:9) * @param ratioY consequent, vertical component of the ratio (e.g. 9 in 16:9) * @return this for chaining */ public RatioKeyline setRatio(@IntRange(from = 0) int ratioX, @IntRange(from = 0) int ratioY) { mRatioX = ratioX; mRatioY = ratioY; return this; } /** * Set arbitrary label text to this ratio keyline. Defaults to displaying ratio. * * @param text Text to display in keyline label. Set null to reset the label to display ratio. * @return this for chaining */ public RatioKeyline setText(@Nullable String text) { mText = text; return this; } /** * Set ratio keyline thickness * * @param thickness Ratio keyline thickness, in pixels. At the moment, ratio keyline will be drawn within enclosed * bounds, so that regardless of thickness it doesn't cover pixels not within ratio box. * @return this for chaining */ public RatioKeyline setThickness(@IntRange(from = 1) int thickness) { mThickness = thickness; return this; } /** * Set the color of ratio keyline and label background * * @param color Ratio keyline color, in #AARRGGBB format as usual * @return this for chaining */ public RatioKeyline setKeylineColor(@ColorInt int color) { mBackgroundPaint.setColor(color); return this; } /** * Set ratio keyline label text color * * @param color Label text color, in #AARRGGBB format as usual * @return this for chaining */ public RatioKeyline setTextColor(@ColorInt int color) { mTextPaint.setColor(color); return this; } @Override public void draw(Canvas canvas, Rect drawableBounds) { final int distanceTop; if (mRatioX == 0) { distanceTop = drawableBounds.top; } else { distanceTop = drawableBounds.top + drawableBounds.width() * mRatioY / mRatioX; } if (distanceTop > drawableBounds.height()) { return; } // Draw keyline canvas.drawRect(drawableBounds.left, distanceTop - mThickness, drawableBounds.right, distanceTop, mBackgroundPaint); // If no special text is set, display ratio if (mText == null) { mText = String.format(Locale.getDefault(), "%d:%d", mRatioX, mRatioY); } // Determine keyline label size/bounds StaticLayout layout = new StaticLayout(mText, mTextPaint, Integer.MAX_VALUE, Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false); int labelTextWidth = (int) (layout.getLineMax(0) + 0.5); // If text too big, re-measure while (labelTextWidth > mLabelRectWidth) { mTextPaint.setTextSize(mTextPaint.getTextSize() * 0.8f); layout = new StaticLayout(mText, mTextPaint, Integer.MAX_VALUE, Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false); labelTextWidth = (int) (layout.getLineMax(0) + 0.5); } // Draw label canvas.save(); canvas.clipRect(drawableBounds.left, 0, drawableBounds.right, distanceTop - mThickness); canvas.translate(drawableBounds.right, distanceTop); canvas.drawPath(mLabelPath, mBackgroundPaint); canvas.restore(); // Determine text position (on the baseline, in the center of canvas.save(); mTempRect.set(-mLabelRectWidth, -mLabelHeight, 0, 0); Gravity.apply(Gravity.CENTER, labelTextWidth, layout.getHeight(), mTempRect, mTempRect); canvas.translate(drawableBounds.right + mTempRect.left, distanceTop + mTempRect.top); layout.draw(canvas); canvas.restore(); } /** * A default factory that creates new {@link RatioKeyline} layers from config lines according to the docs */ public static class Factory implements RhythmSpecLayerFactory { public static final String LAYER_TYPE = "ratio-keyline"; public static final String ARG_RATIO = "ratio"; public static final String ARG_TEXT = "text"; public static final String ARG_THICKNESS = "thickness"; public static final String ARG_COLOR = "color"; public static final String ARG_TEXT_COLOR = "text-color"; private static Pattern RATIO_VALUE_PATTERN = Pattern.compile("(\\d+):(\\d+)"); @Override public RatioKeyline getForArguments(ArgumentsBundle argsBundle) { RatioKeyline keyline = new RatioKeyline(argsBundle.getDisplayMetrics()); String ratio = argsBundle.getString(ARG_RATIO); if (ratio == null) { throw new RhythmInflationException( "Error when inflating ratio-keyline: 'ratio' argument is missing" ); } Matcher matcher = RATIO_VALUE_PATTERN.matcher(ratio); if (!matcher.matches()) { throw new RhythmInflationException( "Error when inflating ratio-keyline: 'ratio' argument is invalid, expected \"x:y\" pattern" ); } keyline.mRatioX = Integer.parseInt(matcher.group(1)); keyline.mRatioY = Integer.parseInt(matcher.group(2)); keyline.mText = argsBundle.getString(ARG_TEXT, ratio); keyline.mThickness = argsBundle.getDimensionPixelSize(ARG_THICKNESS, DEFAULT_THICKNESS); keyline.mBackgroundPaint.setColor(argsBundle.getColor(ARG_COLOR, DEFAULT_FILL_COLOR)); keyline.mTextPaint.setColor(argsBundle.getColor(ARG_TEXT_COLOR, DEFAULT_TEXT_COLOR)); return keyline; } } } ================================================ FILE: rhythm-control/.gitignore ================================================ /build ================================================ FILE: rhythm-control/build.gradle ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ apply plugin: 'com.android.library' apply plugin: 'maven' android { compileSdkVersion 23 buildToolsVersion "23.0.3" defaultConfig { minSdkVersion 8 targetSdkVersion 23 versionCode project.versionCode versionName project.releaseVersion } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } lintOptions { disable 'RtlHardcoded','UnusedAttribute' } } configurations { javadocDeps } task androidJavadocs(type: Javadoc) { source = android.sourceSets.main.java.srcDirs classpath += configurations.javadocDeps classpath += project.files(android.bootClasspath) } task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) { classifier = 'javadoc' from androidJavadocs.destinationDir } task androidSourcesJar(type: Jar) { classifier = 'sources' from android.sourceSets.main.java.srcDirs } artifacts { archives androidSourcesJar archives androidJavadocsJar } afterEvaluate { androidJavadocs.classpath += files(android.libraryVariants.collect { variant -> variant.javaCompile.classpath.files }) } uploadArchives { repositories.mavenDeployer { pom.groupId = 'com.actinarium.rhythm' pom.artifactId = 'rhythm-control' pom.version = project.releaseVersion pom.project { name 'Rhythm Control' description 'A module for Rhythm library that allows controlling Rhythm groups and overlays via a notification' url 'https://github.com/Actinarium/Rhythm' inceptionYear '2015' licenses { license { name 'The Apache Software License, Version 2.0' url 'http://www.apache.org/licenses/LICENSE-2.0.txt' distribution 'repo' } } organization { name 'Actinarium' url 'http://actinarium.com' } developers { developer { id 'Actine' name 'Paul Danyliuk' url 'https://plus.google.com/u/0/+PaulDanyliuk' roles { role 'architect' role 'developer' } } } scm { url 'https://github.com/Actinarium/Rhythm.git' connection 'scm:git:https://github.com/Actinarium/Rhythm.git' developerConnection 'scm:git:git@github.com:Actinarium/Rhythm.git' } } repository(url: "file://D:/Build") } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:support-v4:23.3.0' compile project(':rhythm') javadocDeps 'com.android.support:support-v4:23.3.0' javadocDeps project(':rhythm') } ================================================ FILE: rhythm-control/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in C:\Program Files (x86)\Android\android-sdk/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} ================================================ FILE: rhythm-control/src/main/AndroidManifest.xml ================================================ ================================================ FILE: rhythm-control/src/main/java/com/actinarium/rhythm/control/RhythmControl.java ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.actinarium.rhythm.control; import android.app.Application; import android.content.Context; import android.support.annotation.Nullable; import java.util.ArrayList; import java.util.List; /** *

A controller that interconnects {@link RhythmGroup}s, {@link RhythmFrameLayout}s, and the Quick Control * notification, and should be used as an entry point to accessing Rhythm library programmatically. For proper function * a singleton Rhythm control must be accessible from ApplicationContext (i.e. the app’s {@link Application} object must * implement {@link Host}).

Note: if you don’t need the notification or RhythmicFrameLayouts, * you might actually not need a Rhythm control in your project — just instantiate and use {@link RhythmGroup}s * directly.

* * @author Paul Danyliuk */ public final class RhythmControl { public static int NOTIFICATION_OFF = -2; public static int NOTIFICATION_NO_GROUPS = -1; /** * Context is used for the sole purpose of talking to the {@link RhythmNotificationService}. I sure hope it’s not * leaking. */ @Nullable private Context mContext; private int mCurrentNotificationGroupIndex = NOTIFICATION_OFF; private int mNotificationId; /** * A list of Rhythm groups registered in this control */ private List mRhythmGroups; /** * Create a Rhythm control. Normally you shouldn’t create more than one Rhythm control in your application. * * @param context Current context (usually the {@link Application} object where Rhythm control setup is performed). *
You may actually set this to null if you don’t need the Quick Control * notification. */ public RhythmControl(@Nullable Context context) { mContext = context; mRhythmGroups = new ArrayList<>(); } /** * Make a new Rhythm group, registered in this Rhythm control * * @param title A convenient title for this group to identify it in the notification. Not mandatory (can be * null) but recommended. * @return The created Rhythm group instance, managed by this control */ public RhythmGroup makeGroup(String title) { final RhythmGroup group = new RhythmGroup(); group.mTitle = title; group.mIndex = mRhythmGroups.size(); group.mControl = this; mRhythmGroups.add(group); // If this was the first group, and the notification is already shown, set it to display the first group if (mCurrentNotificationGroupIndex == NOTIFICATION_NO_GROUPS) { mCurrentNotificationGroupIndex = 0; requestNotificationUpdate(); } return group; } /** * Get Rhythm group at requested index * * @param index index of the group (0, 1, 2... in order of adding) * @return requested Rhythm group */ public RhythmGroup getGroup(int index) { return mRhythmGroups.get(index); } /** * @return the number of groups registered in this control */ public int getGroupCount() { return mRhythmGroups.size(); } /** *

Show the “Quick Control” notification, which allows to switch overlays for all registered Rhythm * groups quickly without navigating away from your app. Usually you would want to call this once during initial * configuration (unless you don’t need the notification).

Note: Quick Control notification is * dismissible. Upon dismiss, all Rhythm overlays will be hidden. There’s no way to bring it back other than kill * and restart the application, unless you explicitly create a button, a menu option etc in your application that * would conjure the notification again by calling this method.

* * @param notificationId ID for Rhythm notification, must be unique across the app */ public void showQuickControl(int notificationId) { if (mContext == null) { return; } // Remember the notification ID for reuse on update mNotificationId = notificationId; // If notification isn't displayed already, display the first group (or no groups notice) if (mCurrentNotificationGroupIndex == NOTIFICATION_OFF) { mCurrentNotificationGroupIndex = mRhythmGroups.isEmpty() ? NOTIFICATION_NO_GROUPS : 0; } requestNotificationUpdate(); } /** * Sets all registered drawables in all managed groups to display no Rhythm overlays; sets notification state to * hidden */ void onNotificationDismiss() { mCurrentNotificationGroupIndex = NOTIFICATION_OFF; for (int i = 0, size = mRhythmGroups.size(); i < size; i++) { mRhythmGroups.get(i).selectOverlay(RhythmGroup.NO_OVERLAY); } } /** * Should be called whenever notification state is changed (e.g. when cycling through the groups or overlays) */ void requestNotificationUpdate() { if (mCurrentNotificationGroupIndex != NOTIFICATION_OFF) { RhythmNotificationService.showNotification(mContext, mNotificationId); } } RhythmGroup getCurrentNotificationGroup() { return mCurrentNotificationGroupIndex < 0 ? null : mRhythmGroups.get(mCurrentNotificationGroupIndex); } void selectNextNotificationGroup() { // Assume that this method can be called only when valid notification is displayed for a group with index >= 0 // Increment by 1 and wrap if that was the last one. And request notification update mCurrentNotificationGroupIndex = ++mCurrentNotificationGroupIndex % mRhythmGroups.size(); requestNotificationUpdate(); } /** * The {@link Application} must implement this interface to provide the singleton {@link RhythmControl} instance * through its method {@link #getRhythmControl()} to {@link RhythmFrameLayout}s and the Quick Control notification */ public interface Host { /** * Get the {@link RhythmControl} of this application to access any {@link RhythmGroup} and the rest of Rhythm * API * * @return Rhythm control associated with this application */ RhythmControl getRhythmControl(); } } ================================================ FILE: rhythm-control/src/main/java/com/actinarium/rhythm/control/RhythmFrameLayout.java ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.actinarium.rhythm.control; import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Build; import android.support.annotation.IntDef; import android.support.annotation.Nullable; import android.util.AttributeSet; import android.widget.FrameLayout; import com.actinarium.rhythm.RhythmDrawable; import com.actinarium.rhythm.RhythmOverlay; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * A {@link FrameLayout} implementation with rich Rhythm support. You can use this layout to wrap existing views and * draw a Rhythm overlay from specified group. The overlay can be positioned either under the view, over the view, or * just under/over the content (see {@link #setOverlayPosition(int)}). Both the group and overlay position can be set in * the layout XML with attributes app:rhythmGroup and app:overlayPosition respectively. * * @author Paul Danyliuk * @version $Id$ */ public class RhythmFrameLayout extends FrameLayout { /** * Use this value to indicate that this view is not connected to any {@link RhythmGroup} and shouldn’t display any * overlay */ public static final int NO_GROUP = -1; // The following are to control where the overlay will be drawn // todo: test how overlay position affects its properties on view's translation, rotation, scaling etc /** * Draw the overlay under the background of this view. Pretty useless if this view has opaque background. */ public static final int OVERLAY_POSITION_UNDER_BACKGROUND = 0; /** * Draw the overlay over view’s background but under child views. Default choice: useful yet non-obtrusive. */ public static final int OVERLAY_POSITION_UNDER_CONTENT = 1; /** * Draw the overlay over the view’s content (sans foreground). Use this mode if you have nested opaque views that * occlude the overlay, and there are elements within, which you still need to align. */ public static final int OVERLAY_POSITION_OVER_CONTENT = 2; /** * Same as {@link #OVERLAY_POSITION_OVER_CONTENT}, but this also draws over any foreground (ripples, touch * highlights etc). */ public static final int OVERLAY_POSITION_OVER_FOREGROUND = 3; /** * Index of the group this view should get its {@link RhythmDrawable} from, or {@link #NO_GROUP}. */ protected int mRhythmGroupIndex; @OverlayPosition protected int mOverlayPosition; /** * Obtained from {@link RhythmGroup}, which then controls this drawable, telling it what {@link RhythmOverlay} to * draw. Or null. */ protected RhythmDrawable mRhythmDrawable; /** * Overlay bounds, relative to this view (since canvas is already translated to the origin point of this view) */ protected Rect mBounds = new Rect(); // Constructors public RhythmFrameLayout(Context context) { super(context); mRhythmGroupIndex = NO_GROUP; mOverlayPosition = OVERLAY_POSITION_UNDER_CONTENT; setWillNotDraw(false); } public RhythmFrameLayout(Context context, AttributeSet attrs) { super(context, attrs); initFromAttrs(context, attrs, 0, 0); } public RhythmFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initFromAttrs(context, attrs, defStyleAttr, 0); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public RhythmFrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); initFromAttrs(context, attrs, defStyleAttr, defStyleRes); } private void initFromAttrs(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { TypedArray array = context .getTheme() .obtainStyledAttributes(attrs, R.styleable.RhythmFrameLayout, defStyleAttr, defStyleRes); try { int position = array.getInteger(R.styleable.RhythmFrameLayout_overlayPosition, OVERLAY_POSITION_UNDER_CONTENT); if (position == OVERLAY_POSITION_UNDER_BACKGROUND || position == OVERLAY_POSITION_UNDER_CONTENT || position == OVERLAY_POSITION_OVER_CONTENT || position == OVERLAY_POSITION_OVER_FOREGROUND) { //noinspection ResourceType mOverlayPosition = position; // We need to ensure draw()/onDraw() is called when overlay position is other than over content (otherwise it's drawn elsewhere) setWillNotDraw(position == OVERLAY_POSITION_OVER_CONTENT); } else { mOverlayPosition = OVERLAY_POSITION_UNDER_CONTENT; setWillNotDraw(false); } mRhythmGroupIndex = array.getInteger(R.styleable.RhythmFrameLayout_rhythmGroup, NO_GROUP); } finally { array.recycle(); } } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (changed) { mBounds.set(0, 0, right - left, bottom - top); } if (mRhythmDrawable != null && changed) { // If there's a drawable, and layout has changed, we need to update its bounds mRhythmDrawable.setBounds(mBounds); } else if (mRhythmDrawable == null && mRhythmGroupIndex != NO_GROUP) { // If the group is set but there's no drawable yet, try to pull it from the group onRhythmGroupSet(); } } @Override public void draw(Canvas canvas) { // Draw before or after everything? (if there's anything to draw) if (mRhythmDrawable == null) { super.draw(canvas); } else if (mOverlayPosition == OVERLAY_POSITION_UNDER_BACKGROUND) { mRhythmDrawable.draw(canvas); super.draw(canvas); } else if (mOverlayPosition == OVERLAY_POSITION_OVER_FOREGROUND) { super.draw(canvas); mRhythmDrawable.draw(canvas); } else { super.draw(canvas); } } @Override protected void onDraw(Canvas canvas) { // Draw before content? (if there's anything to draw) if (mRhythmDrawable != null && mOverlayPosition == OVERLAY_POSITION_UNDER_CONTENT) { mRhythmDrawable.draw(canvas); } super.onDraw(canvas); } @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); // Draw over content and children? (if there's anything to draw) if (mRhythmDrawable != null && mOverlayPosition == OVERLAY_POSITION_OVER_CONTENT) { mRhythmDrawable.draw(canvas); } } @Override protected boolean verifyDrawable(Drawable who) { return (mRhythmDrawable != null && mRhythmDrawable == who) || super.verifyDrawable(who); } // Getters/setters /** * @return Current Rhythm drawable */ public RhythmDrawable getRhythmDrawable() { return mRhythmDrawable; } /** * Set a different {@link RhythmDrawable} to this view. * * @param drawable Rhythm drawable to set, or null to unlink this view from Rhythm */ public void setRhythmDrawable(@Nullable RhythmDrawable drawable) { doSetRhythmDrawable(drawable); invalidate(); } /** * @return Index of the Rhythm group this view is linked to, or {@link #NO_GROUP} */ public int getRhythmGroupIndex() { return mRhythmGroupIndex; } /** * Link this drawable to a different {@link RhythmGroup} (identified by index), or to no group at all. * * @param rhythmGroupIndex Index of required {@link RhythmGroup} in {@link RhythmControl}, or {@link #NO_GROUP} */ public void setRhythmGroupIndex(int rhythmGroupIndex) { mRhythmGroupIndex = rhythmGroupIndex; if (mRhythmGroupIndex != NO_GROUP) { onRhythmGroupSet(); } else { doSetRhythmDrawable(null); } invalidate(); } /** * @return Overlay position * @see #OVERLAY_POSITION_UNDER_BACKGROUND * @see #OVERLAY_POSITION_UNDER_CONTENT * @see #OVERLAY_POSITION_OVER_CONTENT * @see #OVERLAY_POSITION_OVER_FOREGROUND */ public int getOverlayPosition() { return mOverlayPosition; } /** * Set overlay position * * @param overlayPosition New overlay position, one of overlay position constants * @see #OVERLAY_POSITION_UNDER_BACKGROUND * @see #OVERLAY_POSITION_UNDER_CONTENT * @see #OVERLAY_POSITION_OVER_CONTENT * @see #OVERLAY_POSITION_OVER_FOREGROUND */ public void setOverlayPosition(@OverlayPosition int overlayPosition) { if (mOverlayPosition != overlayPosition) { mOverlayPosition = overlayPosition; setWillNotDraw(overlayPosition == OVERLAY_POSITION_OVER_CONTENT); invalidate(); } } /** * Retrieves proper drawable from current group and links it to this view */ private void onRhythmGroupSet() { // Request rhythm group from application context Context context = getContext().getApplicationContext(); if (context instanceof RhythmControl.Host) { // This may fail with index out of bounds exception if incorrect group index is provided // But, IMHO, it is better to throw the exception than suppress it and leave the developer clueless final RhythmDrawable drawable = ((RhythmControl.Host) context).getRhythmControl() .getGroup(mRhythmGroupIndex) .makeDrawable(); doSetRhythmDrawable(drawable); } else { // Uh-oh throw new ClassCastException(this + " cannot connect to RhythmControl. " + "Check if your Application implements RhythmControl.Host"); } } /** * Links new drawable to this view */ private void doSetRhythmDrawable(@Nullable RhythmDrawable drawable) { if (mRhythmDrawable != null) { mRhythmDrawable.setCallback(null); } mRhythmDrawable = drawable; if (mRhythmDrawable != null) { mRhythmDrawable.setBounds(mBounds); mRhythmDrawable.setCallback(this); } } /** * Type def annotation for overlay position enum */ @Retention(RetentionPolicy.SOURCE) @IntDef({OVERLAY_POSITION_UNDER_BACKGROUND, OVERLAY_POSITION_UNDER_CONTENT, OVERLAY_POSITION_OVER_CONTENT, OVERLAY_POSITION_OVER_FOREGROUND}) public @interface OverlayPosition { } } ================================================ FILE: rhythm-control/src/main/java/com/actinarium/rhythm/control/RhythmGroup.java ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.actinarium.rhythm.control; import android.graphics.drawable.Drawable; import android.view.View; import android.widget.FrameLayout; import com.actinarium.rhythm.RhythmDrawable; import com.actinarium.rhythm.RhythmOverlay; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.LinkedList; import java.util.List; /** *

Controls a group of {@link RhythmDrawable}s, namely propagates the same {@link RhythmOverlay} for all registered * RhythmDrawables to render. A {@link RhythmGroup} object holds onto a list of RhythmOverlays * and allows cycling through them. Usually, Rhythm groups are used within a {@link RhythmControl} and instantiated via * {@link RhythmControl#makeGroup(String)}) method, however you are free to make “orphaned” groups and * control them explicitly from your app.

Also contains convenience methods for easy decoration of existing views * — those can simplify the scenario when you don’t want to include Rhythm into production builds.

* * @author Paul Danyliuk */ public final class RhythmGroup { public static int NO_OVERLAY = -1; private static final int ESTIMATED_OVERLAYS_PER_GROUP = 4; String mTitle; // Assigned by RhythmControl upon instantiation via {@link RhythmControl#makeGroup(String)}; makes no sense otherwise int mIndex; RhythmControl mControl; private List> mDrawables; private List mOverlays; private int mCurrentOverlayIndex = NO_OVERLAY; /** *

Create a new Rhythm group.

Heads up: do not explicitly call new RhythmGroup() unless * you specifically don’t want it attached to a {@link RhythmControl} (i.e. don’t want it to appear in the Quick * Control notification). Instead, you should use {@link RhythmControl#makeGroup(String)}.

*/ public RhythmGroup() { mDrawables = new LinkedList<>(); mOverlays = new ArrayList<>(ESTIMATED_OVERLAYS_PER_GROUP); } /** * Set the title for this group. The title is only displayed in Rhythm control notification — you don't need it in * programmatically created anonymous groups. * * @param title A convenient title for this group. * @return this for chaining */ public RhythmGroup setTitle(String title) { mTitle = title; return this; } /** * Get group title * * @return group title of null if the group is anonymous */ public String getTitle() { return mTitle; } /** * Add Rhythm overlay to this group * * @param overlay The Rhythm overlay to add * @return this for chaining */ public RhythmGroup addOverlay(RhythmOverlay overlay) { mOverlays.add(overlay); if (mCurrentOverlayIndex == NO_OVERLAY) { selectOverlay(0); } return this; } /** * Add multiple Rhythm overlays to this group * * @param overlays The Rhythm overlays to add * @return this for chaining */ public RhythmGroup addOverlays(Collection overlays) { mOverlays.addAll(overlays); if (mCurrentOverlayIndex == NO_OVERLAY) { selectOverlay(0); } return this; } /** * Make a new {@link RhythmDrawable} that will draw the group’s active {@link RhythmOverlay} and can be used as any * other {@link Drawable} in Android SDK. You must always make separate drawables for using them in different * places, as reusing the same drawable instance may lead to unexpected results. * * @return A new {@link RhythmDrawable} controlled by this group. */ public RhythmDrawable makeDrawable() { RhythmDrawable drawable = new RhythmDrawable(getCurrentOverlay()); mDrawables.add(new WeakReference<>(drawable)); return drawable; } /** *

A handy method that will decorate provided views with {@link RhythmDrawable}s controlled by this group.

*

Note: the backgrounds of all provided views will be wrapped and replaced by * RhythmDrawables. To obtain the original background drawables you have to call {@link * RhythmDrawable#getDecorated() view.getBackground().getDecorated()}.

* * @param views Views whose backgrounds should be decorated with Rhythm drawables * @see #decorateForeground(FrameLayout...) */ @SuppressWarnings("deprecation") public void decorate(View... views) { for (View view : views) { RhythmDrawable decoratingRhythmDrawable = makeDrawable(); decoratingRhythmDrawable.setDecorated(view.getBackground()); view.setBackgroundDrawable(decoratingRhythmDrawable); } } /** * Similar to {@link #decorate(View...)}, but decorates foregrounds instead of backgrounds of provided views * (available only for {@link FrameLayout}), therefore drawing the overlay over the view’s content. Similarly to * decorate(View...), wraps and replaces existing foreground drawable with {@link RhythmDrawable}. * * @param views Frame layouts whose foregrounds should be decorated * @see #decorate(View...) */ public void decorateForeground(FrameLayout... views) { for (FrameLayout view : views) { RhythmDrawable decoratingRhythmDrawable = makeDrawable(); decoratingRhythmDrawable.setDecorated(view.getForeground()); view.setForeground(decoratingRhythmDrawable); } } /** * @return Index of currently selected overlay, or {@link #NO_OVERLAY} * @see #getCurrentOverlay() * @see #getOverlayCount() */ public int getCurrentOverlayIndex() { return mCurrentOverlayIndex; } /** * @return Number of overlays associated with this group * @see #getCurrentOverlayIndex() */ public int getOverlayCount() { return mOverlays.size(); } /** * @return Currently selected overlay, or null if overlay is disabled * @see #getCurrentOverlayIndex() */ public RhythmOverlay getCurrentOverlay() { return mCurrentOverlayIndex != NO_OVERLAY ? mOverlays.get(mCurrentOverlayIndex) : null; } /** * Select overlay by index. Provide {@link #NO_OVERLAY} to hide overlay. * * @param index Overlay index, or {@link #NO_OVERLAY} * @see #selectNextOverlay() * @see #getOverlayCount() */ public void selectOverlay(int index) { if (index == NO_OVERLAY || (index >= 0 && index < mOverlays.size())) { if (mCurrentOverlayIndex != index) { mCurrentOverlayIndex = index; doSetOverlay(getCurrentOverlay()); } } else { throw new IndexOutOfBoundsException("The index is neither NO_OVERLAY nor valid."); } } /** * Convenience method to cycle through overlays. Meant primarily for use in Quick Control notification, but can be * invoked programmatically. * * @see #selectOverlay(int) */ public void selectNextOverlay() { if (mCurrentOverlayIndex == NO_OVERLAY) { if (mOverlays.isEmpty()) { // Still no overlay, so no-op. return; } mCurrentOverlayIndex = 0; } else { mCurrentOverlayIndex = ++mCurrentOverlayIndex % mOverlays.size(); // Disabling overlay after the last one if (mCurrentOverlayIndex == 0) { mCurrentOverlayIndex = NO_OVERLAY; } } doSetOverlay(getCurrentOverlay()); } @Override public String toString() { return mTitle != null ? mTitle : "Group #" + mIndex; } /** * Propagates current overlay to all linked {@link RhythmDrawable}s, removing dead references on the way. Also * updates the notification to reflect current overlay’s name * * @todo add possibility to propagate arbitrary overlay, not just one of those in the list */ private void doSetOverlay(RhythmOverlay overlay) { // Using iterator here because we need to remove elements halfway Iterator> iterator = mDrawables.iterator(); while (iterator.hasNext()) { final RhythmDrawable item = iterator.next().get(); if (item == null) { // Clean up dead references iterator.remove(); } else { item.setOverlay(overlay); } } // If this group is attached to control, request Quick Control notification update if (mControl != null) { mControl.requestNotificationUpdate(); } } } ================================================ FILE: rhythm-control/src/main/java/com/actinarium/rhythm/control/RhythmNotificationService.java ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.actinarium.rhythm.control; import android.app.Application; import android.app.IntentService; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.os.Handler; import android.os.Looper; import android.support.v4.app.NotificationCompat; import com.actinarium.rhythm.RhythmOverlay; /** * An intent service backing Quick Control notification functionality. Requires {@link Application} to implement {@link * RhythmControl.Host} to access the application’s {@link RhythmControl} singleton instance * * @author Paul Danyliuk */ public class RhythmNotificationService extends IntentService { static final String ACTION_SHOW_QUICK_CONTROL = "com.actinarium.rhythm.action.SHOW_QUICK_CONTROL"; static final String ACTION_NEXT_GROUP = "com.actinarium.rhythm.action.NEXT_GROUP"; static final String ACTION_NEXT_OVERLAY = "com.actinarium.rhythm.action.NEXT_OVERLAY"; static final String ACTION_DISMISS_QUICK_CONTROL = "com.actinarium.rhythm.action.DISMISS_QUICK_CONTROL"; static final String EXTRA_NOTIFICATION_ID = "com.actinarium.rhythm.extra.NOTIFICATION_ID"; private static final int NOTIFICATION_ICON_COLOR = 0x105AAE; private static final int NOTIFICATION_ERROR_COLOR = 0xEF4343; public RhythmNotificationService() { super("RhythmService"); } /** * Show the “Quick Control” notification, which allows to switch overlays for all connected Rhythm * groups without leaving the app under development. Can be used to update the notification as well. * * @param context to start the service * @param notificationId Notification ID, must be unique across the app */ static void showNotification(Context context, int notificationId) { Intent intent = new Intent(context, RhythmNotificationService.class); intent.setAction(ACTION_SHOW_QUICK_CONTROL); intent.putExtra(EXTRA_NOTIFICATION_ID, notificationId); context.startService(intent); } @Override protected void onHandleIntent(Intent intent) { if (intent != null) { final String action = intent.getAction(); if (ACTION_SHOW_QUICK_CONTROL.equals(action)) { final int notificationId = intent.getIntExtra(EXTRA_NOTIFICATION_ID, Integer.MIN_VALUE); handleShowNotification(notificationId); } else if (ACTION_NEXT_OVERLAY.equals(action)) { handleNextOverlay(); } else if (ACTION_NEXT_GROUP.equals(action)) { handleNextGroup(); } else if (ACTION_DISMISS_QUICK_CONTROL.equals(action)) { handleDismissQuickConfig(); } } } private void handleShowNotification(int notificationId) { Application application = getApplication(); NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); Notification notification; // If the application is not the host - show error and return. if (!(application instanceof RhythmControl.Host)) { notification = makeCommonNotification(getString(R.string.arl_no_host_text)) .setColor(NOTIFICATION_ERROR_COLOR) .setContentTitle(getString(R.string.arl_no_host_title)) .build(); manager.notify(notificationId, notification); return; } RhythmControl control = ((RhythmControl.Host) application).getRhythmControl(); RhythmGroup currentGroup = control.getCurrentNotificationGroup(); // If there are no groups yet - show warning and return. if (currentGroup == null) { notification = makeCommonNotification(getString(R.string.arl_no_groups_text)) .setColor(NOTIFICATION_ERROR_COLOR) .setContentTitle(getString(R.string.arl_no_groups_title)) .build(); manager.notify(notificationId, notification); return; } // Now if everything is OK: Intent nextGroupAction = new Intent(this, RhythmNotificationService.class); nextGroupAction.setAction(ACTION_NEXT_GROUP); PendingIntent piNextGroupAction = PendingIntent.getService(this, 0, nextGroupAction, PendingIntent.FLAG_UPDATE_CURRENT); Intent nextOverlayAction = new Intent(this, RhythmNotificationService.class); nextOverlayAction.setAction(ACTION_NEXT_OVERLAY); PendingIntent piNextOverlayAction = PendingIntent.getService(this, 0, nextOverlayAction, PendingIntent.FLAG_UPDATE_CURRENT); Intent dismissAction = new Intent(this, RhythmNotificationService.class); dismissAction.setAction(ACTION_DISMISS_QUICK_CONTROL); PendingIntent piDismissAction = PendingIntent.getService(this, 0, dismissAction, PendingIntent.FLAG_UPDATE_CURRENT); // todo: another action when notification is clicked (control activity will be added in v1.0) // Determine what to write in notification RhythmOverlay currentOverlay = currentGroup.getCurrentOverlay(); String groupText = getString(R.string.arl_group, currentGroup.toString()); String overlayText = currentOverlay == null ? getString(R.string.arl_no_overlay) : getString(R.string.arl_overlay, currentOverlay.toString()); // Finally, build and display the notification notification = makeCommonNotification(overlayText) .setColor(NOTIFICATION_ICON_COLOR) .setContentTitle(groupText) .setDeleteIntent(piDismissAction) .addAction(new NotificationCompat.Action(R.drawable.arl_loop, getString(R.string.arl_next_group), piNextGroupAction)) .addAction(new NotificationCompat.Action(R.drawable.arl_loop, getString(R.string.arl_next_overlay), piNextOverlayAction)) .build(); manager.notify(notificationId, notification); } private NotificationCompat.Builder makeCommonNotification(String text) { final NotificationCompat.Builder builder = new NotificationCompat.Builder(this) .setSmallIcon(R.drawable.arl_rhythm) .setCategory(NotificationCompat.CATEGORY_SERVICE) .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setAutoCancel(false) .setShowWhen(false) .setContentText(text) .setStyle(new NotificationCompat.BigTextStyle().bigText(text)); // Old androids throw an exception when the notification doesn't have content intent if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB) { Intent contentAction = new Intent(this, RhythmNotificationService.class); contentAction.setAction(ACTION_NEXT_OVERLAY); PendingIntent piContentAction = PendingIntent.getService(this, 0, contentAction, PendingIntent.FLAG_UPDATE_CURRENT); builder.setContentIntent(piContentAction); } return builder; } private void handleNextGroup() { Application application = getApplication(); if (application instanceof RhythmControl.Host) { final RhythmControl rhythmControl = ((RhythmControl.Host) application).getRhythmControl(); Handler handler = new Handler(Looper.getMainLooper()); // Not using anonymous classes here to avoid leaking context handler.post(new NextGroupRunnable(rhythmControl)); } } private void handleNextOverlay() { Application application = getApplication(); if (application instanceof RhythmControl.Host) { final RhythmControl rhythmControl = ((RhythmControl.Host) application).getRhythmControl(); Handler handler = new Handler(Looper.getMainLooper()); handler.post(new NextOverlayRunnable(rhythmControl.getCurrentNotificationGroup())); } } private void handleDismissQuickConfig() { Application application = getApplication(); if (application instanceof RhythmControl.Host) { final RhythmControl rhythmControl = ((RhythmControl.Host) application).getRhythmControl(); Handler handler = new Handler(Looper.getMainLooper()); handler.post(new HideAllOverlaysRunnable(rhythmControl)); } } /** * Runnable to dispatch next group call to the UI thread */ private static class NextGroupRunnable implements Runnable { private RhythmControl mControl; public NextGroupRunnable(RhythmControl control) { mControl = control; } @Override public void run() { mControl.selectNextNotificationGroup(); } } /** * Runnable to dispatch next overlay call to the UI thread */ private static class NextOverlayRunnable implements Runnable { private RhythmGroup mGroup; public NextOverlayRunnable(RhythmGroup group) { mGroup = group; } @Override public void run() { mGroup.selectNextOverlay(); } } /** * Runnable to dispatch hide all overlays call to the UI thread */ private static class HideAllOverlaysRunnable implements Runnable { private RhythmControl mControl; public HideAllOverlaysRunnable(RhythmControl control) { mControl = control; } @Override public void run() { mControl.onNotificationDismiss(); } } } ================================================ FILE: rhythm-control/src/main/res/values/attrs.xml ================================================ ================================================ FILE: rhythm-control/src/main/res/values/strings.xml ================================================ Group: %s Overlay: %s No overlay Group Overlay No groups found Rhythm control has no groups attached Couldn’t connect to Rhythm Check if your Application implements RhythmManager.Host ================================================ FILE: sample/.gitignore ================================================ /build *iml ================================================ FILE: sample/build.gradle ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ apply plugin: 'com.android.application' android { compileSdkVersion 23 buildToolsVersion "23.0.3" defaultConfig { applicationId "com.actinarium.rhythm.sample" minSdkVersion 8 targetSdkVersion 23 versionCode project.versionCode versionName project.releaseVersion } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:23.3.0' compile 'com.android.support:cardview-v7:23.3.0' compile 'com.actinarium.aligned:aligned:0.1' compile project(':rhythm') compile project(':rhythm-control') } ================================================ FILE: sample/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in C:\Program Files (x86)\Android\android-sdk/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} ================================================ FILE: sample/src/main/AndroidManifest.xml ================================================ ================================================ FILE: sample/src/main/java/com/actinarium/rhythm/sample/MainActivity.java ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.actinarium.rhythm.sample; import android.os.Bundle; import android.support.v7.app.ActionBar; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.CardView; import android.support.v7.widget.Toolbar; import android.text.method.LinkMovementMethod; import android.view.View; import android.widget.LinearLayout; import android.widget.TextView; import com.actinarium.rhythm.control.RhythmControl; import com.actinarium.rhythm.control.RhythmGroup; import com.actinarium.rhythm.RhythmOverlayInflater; import static com.actinarium.rhythm.sample.RhythmSampleApplication.CARD_OVERLAY_GROUP; import static com.actinarium.rhythm.sample.RhythmSampleApplication.CONTENT_OVERLAY_GROUP; import static com.actinarium.rhythm.sample.RhythmSampleApplication.TEXT_OVERLAY_GROUP; public class MainActivity extends AppCompatActivity { private RhythmSandbox mSandbox; @SuppressWarnings("deprecation") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Rhythm-unrelated init routines setupToolbar(); setupInteractivity(savedInstanceState); // Find required layouts final LinearLayout contentView = (LinearLayout) findViewById(R.id.content); final CardView cardView = (CardView) findViewById(R.id.card); // Get the groups from the app's RhythmControl. // These groups are already configured with respectable overlays in RhythmSampleApplication#onCreate // All that's left is to link these groups to appropriate views final RhythmControl rhythmControl = ((RhythmSampleApplication) getApplication()).getRhythmControl(); final RhythmGroup contentOverlayGroup = rhythmControl.getGroup(CONTENT_OVERLAY_GROUP); final RhythmGroup cardOverlayGroup = rhythmControl.getGroup(CARD_OVERLAY_GROUP); final RhythmGroup textOverlayGroup = rhythmControl.getGroup(TEXT_OVERLAY_GROUP); // Decorate the background of our topmost scrollable layout (LinearLayout) to draw overlays from the 1st group // The decorate() method works with all views and draws overlay UNDER content (over existing background if any) contentOverlayGroup.decorate(contentView /*, view2, view3... */); // Decorate the foreground of our intermission card with the overlay from the 2nd group // If you have FrameLayout or its child classes, you can use decorateForeground() to draw overlays OVER content cardOverlayGroup.decorateForeground(cardView /*, frameLayout2, frameLayout3... */); // Decorate all text views with overlays attached to the 3rd group for (int i = 0, count = contentView.getChildCount(); i < count; i++) { final View child = contentView.getChildAt(i); if (child instanceof com.actinarium.aligned.TextView) { textOverlayGroup.decorate(child); } } } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); mSandbox.onSaveInstanceState(outState); } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); mSandbox.onRestoreInstanceState(savedInstanceState); } // Methods for setup, not really relevant to teaching you how to set up Rhythm // If you're interested how Rhythm Sandbox works, feel free to check out the eponymous class private void setupToolbar() { final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); final ActionBar actionBar = getSupportActionBar(); assert actionBar != null; actionBar.setElevation(getResources().getDimension(R.dimen.actionBarElevation)); actionBar.setTitle(R.string.app_title); } private void setupInteractivity(Bundle savedInstanceState) { final RhythmSampleApplication application = (RhythmSampleApplication) getApplication(); // Make links clickable ((TextView) findViewById(R.id.copy_2)).setMovementMethod(LinkMovementMethod.getInstance()); ((TextView) findViewById(R.id.copy_6)).setMovementMethod(LinkMovementMethod.getInstance()); // Setup the sandbox presenter - the piece of UI where you can play around with overlay config at runtime. // It needs an inflater - we could build a new one, but we already have the one prepared in RhythmSampleApplication final RhythmOverlayInflater inflater = application.getRhythmOverlayInflater(); final View sandboxRootView = findViewById(R.id.sandbox); mSandbox = new RhythmSandbox(this, sandboxRootView, inflater); if (savedInstanceState == null) { // Call this so that the presenter prepares its initial state mSandbox.onRestoreInstanceState(null); } // Set up "Toggle overlay" button in Intermission card // Usually you'll switch overlays via a notification, but here's the demo of how to do it programmatically final RhythmGroup cardOverlayGroup = application.getRhythmControl().getGroup(CARD_OVERLAY_GROUP); findViewById(R.id.toggle_card_overlay).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // Since there is only one card overlay, selectNextOverlay() will cycle through that one and NO_OVERLAY cardOverlayGroup.selectNextOverlay(); } }); } } ================================================ FILE: sample/src/main/java/com/actinarium/rhythm/sample/RhythmSampleApplication.java ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.actinarium.rhythm.sample; import android.app.Application; import android.view.Gravity; import com.actinarium.rhythm.control.RhythmControl; import com.actinarium.rhythm.control.RhythmGroup; import com.actinarium.rhythm.RhythmOverlay; import com.actinarium.rhythm.RhythmOverlayInflater; import com.actinarium.rhythm.layer.Inset; import com.actinarium.rhythm.sample.customlayers.ImageBox; import com.actinarium.rhythm.sample.customlayers.LayoutBounds; import com.actinarium.rhythm.layer.GridLines; import com.actinarium.rhythm.layer.Keyline; import java.util.List; /** * Application class of Rhythm sample app. For RhythmicFrameLayout and Quick Control notification support, it must * implement RhythmControl.Host * * @author Paul Danyliuk */ public class RhythmSampleApplication extends Application implements RhythmControl.Host { public static final int CONTENT_OVERLAY_GROUP = 0; public static final int CARD_OVERLAY_GROUP = 1; public static final int TEXT_OVERLAY_GROUP = 2; private RhythmControl mRhythmControl; private static final int RHYTHM_NOTIFICATION_ID = -2; private RhythmOverlayInflater mRhythmOverlayInflater; @Override public void onCreate() { super.onCreate(); // Initialize inflater that we'll use to inflate overlays from declarative (human-readable) config mRhythmOverlayInflater = RhythmOverlayInflater.createDefault(this).setMagicVariablesEnabled(true); // Register the factories for our custom layers so that we can inflate them from text config mRhythmOverlayInflater.registerFactory(ImageBox.Factory.LAYER_TYPE, new ImageBox.Factory()); mRhythmOverlayInflater.registerFactory(LayoutBounds.Factory.LAYER_TYPE, new LayoutBounds.Factory()); // Inflate everything from /res/raw/overlay_config. List overlays = mRhythmOverlayInflater.inflate(R.raw.overlay_config); // Initialize this application's Rhythm control. That's for the notification. mRhythmControl = new RhythmControl(this); // Create the groups - that's to control their overlays separately // There may be as many groups as you need, but you need at least one // Groups attached to the control are assigned sequential indices starting at 0 RhythmGroup contentBgGroup = mRhythmControl.makeGroup("All content background"); // index = 0 RhythmGroup cardOverlayGroup = mRhythmControl.makeGroup("Intermission card"); // index = 1 RhythmGroup textOverlayGroup = mRhythmControl.makeGroup("All text labels"); // index = 2 // Overlays 0..4 are for content bg group contentBgGroup.addOverlays(overlays.subList(0, 5)); // Overlay #5 goes to the card group cardOverlayGroup.addOverlay(overlays.get(5)); // And the last overlay goes to text views group textOverlayGroup.addOverlay(overlays.get(6)); // It's also possible to create overlays imperatively, but it's cumbersome and DISCOURAGED. Use inflater instead // Here's how we would build a hybrid grid identical to the one on /res/raw/overlay_config lines 25-32: float density = getResources().getDisplayMetrics().density; // we need this to convert dp to px ourselves... RhythmOverlay unusedOverlay = new RhythmOverlay(5) .addLayer(new GridLines(Gravity.TOP, (int) (4 * density)).setColor(GridLines.DEFAULT_GRID_COLOR)) .addLayer(new Inset(1) .addLayer(new GridLines(Gravity.TOP, (int) (8 * density)) .setOffset((int) (4 * density)) .setColor(GridLines.DEFAULT_GRID_COLOR))) .addLayer(new Keyline(Gravity.LEFT, (int) (16 * density))) .addLayer(new Keyline(Gravity.RIGHT, (int) (16 * density))) .addLayer(new Keyline(Gravity.LEFT, (int) (72 * density))); // By default, if a group has overlays, the first one is initially selected. // Let's hide overlays from these two groups and let you figure out how to enable them back via the notification cardOverlayGroup.selectOverlay(RhythmGroup.NO_OVERLAY); textOverlayGroup.selectOverlay(RhythmGroup.NO_OVERLAY); // Show the notification, and we're all set! mRhythmControl.showQuickControl(RHYTHM_NOTIFICATION_ID); } @Override public RhythmControl getRhythmControl() { return mRhythmControl; } public RhythmOverlayInflater getRhythmOverlayInflater() { return mRhythmOverlayInflater; } } ================================================ FILE: sample/src/main/java/com/actinarium/rhythm/sample/RhythmSandbox.java ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.actinarium.rhythm.sample; import android.app.Activity; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.app.DialogFragment; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.view.View; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.MultiAutoCompleteTextView; import com.actinarium.aligned.Utils; import com.actinarium.rhythm.RhythmDrawable; import com.actinarium.rhythm.RhythmOverlay; import com.actinarium.rhythm.RhythmOverlayInflater; import com.actinarium.rhythm.control.RhythmFrameLayout; import com.actinarium.rhythm.layer.Columns; import com.actinarium.rhythm.layer.DimensionsLabel; import com.actinarium.rhythm.layer.Fill; import com.actinarium.rhythm.layer.GridLines; import com.actinarium.rhythm.layer.Inset; import com.actinarium.rhythm.layer.Keyline; import com.actinarium.rhythm.layer.RatioKeyline; /** * A presenter for the Rhythm Sandbox card, where you can try the configuration at runtime * * @author Paul Danyliuk */ public class RhythmSandbox { /* * Words for auto-complete. Not containing words from custom layers */ String[] ALL_CONFIG_WORDS = { Keyline.Factory.LAYER_TYPE, GridLines.Factory.LAYER_TYPE, Fill.Factory.LAYER_TYPE, Inset.Factory.LAYER_TYPE, RatioKeyline.Factory.LAYER_TYPE, DimensionsLabel.Factory.LAYER_TYPE, Columns.Factory.LAYER_TYPE, "outside", "no-clip", "clip-only", "from=", "distance=", "step=", "ratio=", "gravity=", "count=", "top", "bottom", "left", "right", "top=", "bottom=", "left=", "right=", "width=", "height=", "color=", "color=#", "text=", "limit=", "offset=", "thickness=", "text-color=", "text-size=" }; private static final String DEFAULT_SANDBOX_CONFIG = "@margin=16dp\n" + "@keyline_thickness=1dp\n" + "grid-lines step=8dp from=left\n" + "grid-lines step=4dp from=top\n" + "inset left=0dp width=@margin\n" + " fill\n" + "inset right=0dp width=@margin\n" + " fill\n" + "keyline distance=@margin from=left\n" + "keyline distance=@margin from=right"; // ----------------------------------------------------------------------------------------------------------------- private static final String ARG_RENDER = "com.actinarium.rhythm.sample.arg.RENDER"; private AppCompatActivity mActivity; private RhythmOverlayInflater mOverlayInflater; private MultiAutoCompleteTextView mOverlayConfig; private RhythmFrameLayout mPreview; private boolean mDoRender; /** * Initialize a presenter for sandbox * * @param activity Activity that hosts this sandbox * @param rootView Root view of the sandbox * @param overlayInflater Overlay inflater used to inflate rhythm config */ public RhythmSandbox(AppCompatActivity activity, View rootView, RhythmOverlayInflater overlayInflater) { mActivity = activity; mOverlayInflater = overlayInflater; // Find and init preview layout mPreview = (RhythmFrameLayout) rootView.findViewById(R.id.preview); mPreview.setRhythmDrawable(new RhythmDrawable(null)); // Find and init overlay config text box mOverlayConfig = (MultiAutoCompleteTextView) rootView.findViewById(R.id.config); mOverlayConfig.setHorizontallyScrolling(true); // Fix config text box metrics int i4dp = activity.getResources().getDimensionPixelOffset(R.dimen.i4dp); Utils.setExactMetrics(mOverlayConfig, i4dp * 6, i4dp * 5, i4dp * 3); // Enable auto-complete for config ArrayAdapter adapter = new ArrayAdapter<>(activity, android.R.layout.simple_dropdown_item_1line, ALL_CONFIG_WORDS); mOverlayConfig.setTokenizer(new ConfigTokenizer()); mOverlayConfig.setAdapter(adapter); // Find and init Apply button final Button applyButton = (Button) rootView.findViewById(R.id.apply); applyButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { updatePreview(); } }); } public void onSaveInstanceState(Bundle outState) { outState.putBoolean(ARG_RENDER, mDoRender); } public void onRestoreInstanceState(Bundle savedState) { if (savedState == null) { mOverlayConfig.setText(DEFAULT_SANDBOX_CONFIG); } else { mDoRender = savedState.getBoolean(ARG_RENDER, false); if (mDoRender) { updatePreview(); } } } /** * Take the text out of text field and render an overlay into preview frame */ private void updatePreview() { String overlayConfig = mOverlayConfig.getText().toString(); if (validate(overlayConfig)) { mDoRender = true; final RhythmOverlay overlay = mOverlayInflater.inflateOverlay(overlayConfig); mPreview.getRhythmDrawable().setOverlay(overlay); } } /** * Perform overlay validation, show dialog if issues encountered * * @param overlayConfig Overlay config string to validate * @return true if validation passed */ private boolean validate(String overlayConfig) { String error = null; // If config is empty, short-circuit if (overlayConfig.trim().length() == 0) { error = mActivity.getString(R.string.validation_config_empty); InvalidOverlayDialogFragment dialogFragment = InvalidOverlayDialogFragment.newInstance(error); dialogFragment.show(mActivity.getSupportFragmentManager(), InvalidOverlayDialogFragment.TAG); return false; } // Heads up: line-by-line validation was removed because of increased complexity after 0.9.5. // Thing is, when inflating a raw overlay config file the validation is not really that helpful, // therefore not implementing it as a core feature. try { mOverlayInflater.inflateOverlay(overlayConfig); } catch (Exception e) { error = e.getMessage(); } if (error == null) { return true; } else { InvalidOverlayDialogFragment dialogFragment = InvalidOverlayDialogFragment.newInstance(error); dialogFragment.show(mActivity.getSupportFragmentManager(), InvalidOverlayDialogFragment.TAG); return false; } } /** * Imperfect tokenizer for configuration auto-complete */ private static class ConfigTokenizer implements MultiAutoCompleteTextView.Tokenizer { @Override public int findTokenStart(CharSequence text, int cursor) { int i = cursor; while (i > 0 && text.charAt(i - 1) != ' ' && text.charAt(i - 1) != '=' && text.charAt(i - 1) != '\n') { i--; } return i; } @Override public int findTokenEnd(CharSequence text, int cursor) { int i = cursor; int len = text.length(); while (i < len) { if (text.charAt(i) == ' ' || text.charAt(i) == '=' || text.charAt(i) == '\n') { return i; } else { i++; } } return len; } @Override public CharSequence terminateToken(CharSequence text) { int i = text.length(); if (i > 0 && text.charAt(i - 1) == ' ' || text.charAt(i - 1) == '=' || text.charAt(i - 1) == '\n' || text.charAt(i - 1) == '#') { return text; } else { return text + " "; } } } /** * Dialog for validation errors */ public static class InvalidOverlayDialogFragment extends DialogFragment { public static final String TAG = "InvalidOverlayDialogFragment"; public static final String ARG_ERROR = "com.actinarium.rhythm.sample.intent.arg.ERROR"; private Context mContext; public static InvalidOverlayDialogFragment newInstance(String error) { InvalidOverlayDialogFragment fragment = new InvalidOverlayDialogFragment(); Bundle args = new Bundle(1); args.putString(ARG_ERROR, error); fragment.setArguments(args); return fragment; } @Override public void onAttach(Activity activity) { super.onAttach(activity); mContext = activity; } @Override @NonNull public Dialog onCreateDialog(Bundle savedInstanceState) { final String error = getArguments().getString(ARG_ERROR); AlertDialog.Builder builder = new AlertDialog.Builder(mContext); builder .setTitle(R.string.validation_config_dialog_title) .setMessage(error) .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.cancel(); } }); return builder.create(); } } } ================================================ FILE: sample/src/main/java/com/actinarium/rhythm/sample/customlayers/ImageBox.java ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.actinarium.rhythm.sample.customlayers; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.view.Gravity; import com.actinarium.rhythm.ArgumentsBundle; import com.actinarium.rhythm.RhythmSpecLayer; import com.actinarium.rhythm.RhythmInflationException; import com.actinarium.rhythm.RhythmSpecLayerFactory; import com.actinarium.rhythm.layer.DimensionsLabel; import com.actinarium.rhythm.layer.Inset; /** * An example of a custom spec layer drawing a translucent overlay of specified width and height, gravity and margin, * with dimensions label in the center. In fact, due to the introduction of a more universal {@link Inset}, this * spec layer could be significantly simplified, but all the w/h/gravity/distance parameters are purposefully kept to * provide an example of a more complex custom layer with a rather complex {@link Factory} * * @author Paul Danyliuk */ public class ImageBox implements RhythmSpecLayer { protected int mWidth; protected int mHeight; protected int mDistanceX; protected int mDistanceY; protected int mGravity; protected Rect mTemp; protected Paint mPaint; protected DimensionsLabel mDimensionsLabel; private static final int COLOR = 0x40000000; public ImageBox(int width, int height, int distanceX, int distanceY, int gravity, float scaleFactor) { mWidth = width; mHeight = height; mDistanceX = distanceX; mDistanceY = distanceY; mGravity = gravity; mDimensionsLabel = new DimensionsLabel() .setScaleFactor(scaleFactor) .setGravity(Gravity.CENTER) .setBackgroundColor(Color.TRANSPARENT) .setTextColor(Color.WHITE) .setTextSize(scaleFactor * DimensionsLabel.DEFAULT_TEXT_SIZE); mTemp = new Rect(); mPaint = new Paint(); mPaint.setStyle(Paint.Style.FILL); mPaint.setColor(COLOR); } /** * Private minimalistic constructor for the factory */ private ImageBox(float scaleFactor) { mTemp = new Rect(); mPaint = new Paint(); mPaint.setStyle(Paint.Style.FILL); mPaint.setColor(COLOR); mDimensionsLabel = new DimensionsLabel() .setScaleFactor(scaleFactor) .setGravity(Gravity.CENTER) .setBackgroundColor(Color.TRANSPARENT) .setTextColor(Color.WHITE) .setTextSize(scaleFactor * DimensionsLabel.DEFAULT_TEXT_SIZE); } @Override public void draw(Canvas canvas, Rect drawableBounds) { // Calculate the rect where we should draw the grid Gravity.apply(mGravity, mWidth, mHeight, drawableBounds, mDistanceX, mDistanceY, mTemp); // Draw the box canvas.drawRect(mTemp, mPaint); // Draw dimensions in the center of the box mDimensionsLabel.draw(canvas, mTemp); } /** * A factory to add inflater support for this custom layer. See how you can get various values from the ArgumentsBundle * object */ public static class Factory implements RhythmSpecLayerFactory { public static final String LAYER_TYPE = "image-box"; @Override public ImageBox getForArguments(ArgumentsBundle argsBundle) { ImageBox box = new ImageBox(argsBundle.getDisplayMetrics().density); box.mGravity = argsBundle.getGravity("gravity", Gravity.NO_GRAVITY); if (box.mGravity == Gravity.NO_GRAVITY) { throw new RhythmInflationException( "Error when inflating image-box: 'gravity' argument missing or invalid" ); } box.mWidth = argsBundle.getDimensionPixelSize("width", 0); box.mHeight = argsBundle.getDimensionPixelSize("height", 0); box.mDistanceX = argsBundle.getDimensionPixelOffset("distance-x", 0); box.mDistanceY = argsBundle.getDimensionPixelOffset("distance-y", 0); return box; } } } ================================================ FILE: sample/src/main/java/com/actinarium/rhythm/sample/customlayers/LayoutBounds.java ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.actinarium.rhythm.sample.customlayers; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Region; import com.actinarium.rhythm.ArgumentsBundle; import com.actinarium.rhythm.RhythmSpecLayer; import com.actinarium.rhythm.RhythmInflationException; import com.actinarium.rhythm.RhythmSpecLayerFactory; /** * A simple custom layer that draws layout bounds, similarly to "Draw layout bounds" in Developer options * * @author Paul Danyliuk */ public class LayoutBounds implements RhythmSpecLayer { private Paint mPaint; private RectF mTemp; private int mCrosshairSize; public LayoutBounds(int crosshairSize) { this(); mCrosshairSize = crosshairSize; } private LayoutBounds() { mPaint = new Paint(); mPaint.setStyle(Paint.Style.STROKE); mTemp = new RectF(); } @Override public void draw(Canvas canvas, Rect drawableBounds) { mTemp.set(drawableBounds); mTemp.inset(0.5f, 0.5f); // Draw big box mPaint.setStrokeWidth(0); mPaint.setColor(Color.MAGENTA); canvas.drawRect(mTemp, mPaint); // Draw crosshair mPaint.setStrokeWidth(3); mPaint.setColor(Color.BLUE); canvas.save(); canvas.clipRect(drawableBounds.left, drawableBounds.top, drawableBounds.left + mCrosshairSize, drawableBounds.top + mCrosshairSize); canvas.clipRect(drawableBounds.right - mCrosshairSize, drawableBounds.top, drawableBounds.right, drawableBounds.top + mCrosshairSize, Region.Op.UNION); canvas.clipRect(drawableBounds.left, drawableBounds.bottom - mCrosshairSize, drawableBounds.left + mCrosshairSize, drawableBounds.bottom, Region.Op.UNION); canvas.clipRect(drawableBounds.right - mCrosshairSize, drawableBounds.bottom - mCrosshairSize, drawableBounds.right, drawableBounds.bottom, Region.Op.UNION); canvas.drawRect(mTemp, mPaint); canvas.restore(); } public static class Factory implements RhythmSpecLayerFactory { public static final String LAYER_TYPE = "layout-bounds"; @Override public LayoutBounds getForArguments(ArgumentsBundle argsBundle) { LayoutBounds layoutBounds = new LayoutBounds(); layoutBounds.mCrosshairSize = argsBundle.getDimensionPixelOffset("crosshair-size", 0); if (layoutBounds.mCrosshairSize <= 0) { throw new RhythmInflationException( "Error when inflating layout-bounds: crosshair-size is <= 0 or invalid" ); } return layoutBounds; } } } ================================================ FILE: sample/src/main/java/com/actinarium/rhythm/sample/util/BulletSpan.java ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.actinarium.rhythm.sample.util; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.os.Build; import android.os.Parcel; import android.text.Layout; import android.text.Spanned; import android.text.style.LeadingMarginSpan; /** * Bullet list span, copied from SDK but made more customizable * * @author Paul Danyliuk */ public class BulletSpan implements LeadingMarginSpan { private final int mBulletRadius; private final int mBulletCenterX; private final int mLeadingMargin; private final boolean mWantColor; private final int mColor; private static Path sBulletPath = null; public BulletSpan(int bulletRadius, int bulletCenterX, int leadingMargin) { mBulletRadius = bulletRadius; mBulletCenterX = bulletCenterX; mLeadingMargin = leadingMargin; mWantColor = false; mColor = 0; } public BulletSpan(Parcel src) { mBulletRadius = src.readInt(); mBulletCenterX = src.readInt(); mLeadingMargin = src.readInt(); mWantColor = src.readInt() != 0; mColor = src.readInt(); } public int describeContents() { return 0; } public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mBulletRadius); dest.writeInt(mBulletCenterX); dest.writeInt(mLeadingMargin); dest.writeInt(mWantColor ? 1 : 0); dest.writeInt(mColor); } public int getLeadingMargin(boolean first) { return mLeadingMargin; } public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout l) { if (((Spanned) text).getSpanStart(this) == start) { Paint.Style style = p.getStyle(); int oldColor = 0; if (mWantColor) { oldColor = p.getColor(); p.setColor(mColor); } p.setStyle(Paint.Style.FILL); if (Build.VERSION.SDK_INT >= 11 && c.isHardwareAccelerated()) { if (sBulletPath == null) { sBulletPath = new Path(); sBulletPath.addCircle(0.0f, 0.0f, mBulletRadius, Path.Direction.CW); } c.save(); c.translate(x + dir * mBulletCenterX, (top + bottom) / 2.0f); c.drawPath(sBulletPath, p); c.restore(); } else { c.drawCircle(x + dir * mBulletCenterX, (top + bottom) / 2.0f, mBulletRadius, p); } if (mWantColor) { p.setColor(oldColor); } p.setStyle(style); } } } ================================================ FILE: sample/src/main/java/com/actinarium/rhythm/sample/util/ViewUtils.java ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.actinarium.rhythm.sample.util; import android.text.SpannableStringBuilder; /** * @author Paul Danyliuk */ public final class ViewUtils { private ViewUtils() {} public static CharSequence makeBulletList(int bulletRadius, int bulletCenterX, int leadingMargin, CharSequence... items) { final SpannableStringBuilder builder = new SpannableStringBuilder(); int spanStart = 0; int spanEnd; for (int i = 0, itemsLength = items.length; i < itemsLength; i++) { CharSequence item = items[i]; builder.append(item); if (i != itemsLength - 1) { builder.append("\n"); } spanEnd = builder.length(); builder.setSpan(new BulletSpan(bulletRadius, bulletCenterX, leadingMargin), spanStart, spanEnd, 0); spanStart = spanEnd; } return builder; } } ================================================ FILE: sample/src/main/res/layout/activity_main.xml ================================================