Showing preview only (308K chars total). Download the full file or copy to clipboard to get everything.
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-library) [](https://bintray.com/actinarium/maven/rhythm/_latestVersion) [](https://android-arsenal.com/details/1/2664) [](https://android-arsenal.com/api?level=8) [][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:

## 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:
<a href='https://play.google.com/store/apps/details?id=com.actinarium.materialcue&referrer=utm_source%3Dgh-rhythm%26utm_medium%3Dreferral%26utm_term%3Drhythm-readme'><img alt='Get it on Google Play' src='https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png' height="72" /></a>
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<RhythmOverlay> 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):

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<RhythmOverlay> 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
<com.actinarium.rhythm.control.RhythmFrameLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:rhythmGroup="0"
app:overlayPosition="overContent">
<LinearLayout ... />
</com.actinarium.rhythm.control.RhythmFrameLayout>
```
**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
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.actinarium.rhythm">
<application />
</manifest>
================================================
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<T extends AbstractSpecLayerGroup> implements RhythmSpecLayerParent {
protected static final int ESTIMATED_AVG_LAYERS = 8;
protected List<RhythmSpecLayer> 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. <b>Note:</b> 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 <code>null</code>.
*
* @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 <code>arg</code> are identical to <code>arg=true</code>.
*
* @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
* <b>doesn't</b> 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 <b>down</b> 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 <b>up or
* down</b> 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 <code>@{layer_name}_{arg_name}</code> where dashes are replaced with underscores.
*
* @author Paul Danyliuk
*/
public class MagicVariablesArgumentsBundle extends SimpleArgumentsBundle {
protected String mLayerNamePrefix;
protected Map<String, String> 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.<br>For performance reasons, this map will be used as is, therefore it
* <b>must not</b> 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. <code>grid-lines</code>
* @param metrics Display metrics associated with this arguments bundle, required so that dimension values (dp,
* sp
*/
public MagicVariablesArgumentsBundle(@NonNull Map<String, String> arguments,
@NonNull Map<String, String> 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 <code>@{layer_name}_{arg_name}</code> (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;
/**
* <p>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.</p> <p>For easy
* integration with existing layouts, <code>RhythmDrawable</code> can decorate another <code>Drawable</code> —
* 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. <b>Note:</b> 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.</p>
* <p>Normally you shouldn’t extend this class. If you need to perform custom drawing, consider creating a custom {@link
* RhythmSpecLayer} implementation instead.</p>
*
* @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 <code>null</code>.
*/
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 <code>null</code> 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 <code>null</code> 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 <code>null</code>
*/
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. <b>Note:</b> 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 <code>null</code> 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<RhythmOverlay> {
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 <code>null</code> if the overlay is anonymous
*/
public String getTitle() {
return mTitle;
}
/**
* <p>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.</p> <p><b>Warning:</b> for
* simplicity and performance reasons the same layer objects are used, therefore it’s strongly advised that you
* don’t mutate them after adding.</p>
*
* @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;
/**
* <p>A default inflater that creates {@linkplain RhythmOverlay}s from text configuration using registered layer
* factories. Supports inflating multiple overlays from configuration files (see <a
* href="https://github.com/Actinarium/Rhythm/wiki">the docs</a>) separated by newlines as well as separate overlays;
* supports comments and variables, and supporting custom spec layers by allowing to register spec layer
* factories.</p><p>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.</p>
*
* @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<String, RhythmSpecLayerFactory> mFactories;
protected boolean mAreMagicVariablesEnabled = false;
/**
* <p>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.</p><p>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:</p>
* <pre><code>
* 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());
* </code></pre>
*
* @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);
}
/**
* <p>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: <code>@{layer_name}_{arg_name}</code>, where dashes are replaced with underscores.</p>
* <p><b>Warning:</b>“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.</p>
*
* @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<RhythmOverlay> inflate(@RawRes int rawResId) {
List<String> 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<RhythmOverlay> inflate(String configString) {
List<String> configStrings = Arrays.asList(configString.split("\\r?\\n"));
return inflate(configStrings);
}
/**
* <p>Same as {@link #inflate(int)}, but accepts the configuration file already split in lines as strings.</p>
* <p>This method walks over the lines and determines how the config should be split into separate overlays.</p>
*
* @param configStrings Configuration file split as separate lines. Must follow the same syntax rules as the
* configuration file, that is, no <code>null</code> strings, and overlays being separated by
* an empty line
* @return A list of inflated Rhythm overlays
* @see #inflate(int)
* @see #inflate(String)
*/
public List<RhythmOverlay> inflate(List<String> configStrings) {
List<RhythmOverlay> overlays = new ArrayList<>();
Map<String, String> 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<String> 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<String, String> vars) {
List<String> 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<String> 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<String> configStrings, @NonNull Map<String, String> 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<String> configStrings, @NonNull Map<String, String> 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<String, String> 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.
* <code>@primary=#FF0000</code> to use in <code>color=@primary</code>). Cannot be
* <code>null</code> — pass {@link Collections#EMPTY_MAP} if there are no variables.
* @return inflated layer
*/
public RhythmSpecLayer inflateLayer(String configString, @NonNull Map<String, String> 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.
* <code>@primary=#FF0000</code> to use in <code>color=@primary</code>)
* @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<String, String> 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 <layer_name> <arg1>=<val1> <arg2>=<val2>..."
).setLineNumber(lineNumber);
}
final String specLayerType = matcher.group();
final int spaces = matcher.start();
final Map<String, String> 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<String, String> 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 <code>//</code>) 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;
/**
* <p>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).</p><p>You can create custom spec layers by implementing this interface.</p>
*/
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;
/**
* <p>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
* <code>RhythmSpecLayerFactory</code> and register it within {@link RhythmOverlayInflater#registerFactory(String,
* RhythmSpecLayerFactory)} method.</p><p>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.</p>
*
* @author Paul Danyliuk
*/
public interface RhythmSpecLayerFactory<T extends RhythmSpecLayer> {
/**
* 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<String, String> 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.<br>For performance reasons, this map will be used as is, therefore it
* <b>must not</b> 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<String, String> 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 <code>getXxx()</code> 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
* <code>top</code> or <code>center_vertical</code>
*/
@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} <b>Note:</b> 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} <b>Note:</b> 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} <b>Note:</b> 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} <b>Note:</b> 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. <code>24f</code>
* @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<T extends RhythmSpecLayer> implements RhythmSpecLayerFactory<T> {
private RhythmSpecLayerFactory<T> mDecoratedFactory;
private Map<ArgumentsBundle, T> mCache;
public SimpleCacheFactory(RhythmSpecLayerFactory<T> 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<String> readLines(InputStream inputStream) {
final BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, Charset.defaultCharset()));
ArrayList<String> 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 <code>/res/raw</code> folder without extension
* @return List of lines read from the file
*/
public static List<String> 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<Columns> {
@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;
}
/**
* <p>Create spec layer that will evenly divide current bounds in given number of columns and then draw all child
* layers in each.</p> <p>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.</p>
*/
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 <a
* href="https://github.com/Actinarium/Rhythm/wiki/Declarative-configuration#columns">the docs</a>
*/
public static class Factory implements RhythmSpecLayerFactory<Columns> {
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)}. <b>Experimental at the moment, meaning its behavior, appearance, and parameters may
* change.</b>
*
* @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 <code>sp</code>,
* or {@link #DEFAULT_SCALE_FACTOR} (<code>1f</code>) 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. <code>{@link Gravity#BOTTOM} | {@link
* Gravity#LEFT}</code>
* @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 <a
* href="https://github.com/Actinarium/Rhythm/wiki/Declarative-configuration#dimensions-label">the docs</a>
*/
public static class Factory implements RhythmSpecLayerFactory<DimensionsLabel> {
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 <a
* href="https://github.com/Actinarium/Rhythm/wiki/Declarative-configuration#fill">the docs</a>
*/
public static class Factory implements RhythmSpecLayerFactory<Fill> {
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 <b>or</b> 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. <b>Note:</b> RTL properties are not supported, you only have <i>left</i> and
* <i>right</i> 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 <i>below</i> the delimited pixel row, and vertical lines are always drawn <i>to the right</i> 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 <b>and</b> 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);
}
/**
* <p>Create a spec layer that displays dimensions label.</p> <p>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.</p>
*/
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 <b>and</b> 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 <a
* href="https://github.com/Actinarium/Rhythm/wiki/Declarative-configuration#grid-lines">the docs</a>
*/
public static class Factory implements RhythmSpecLayerFactory<GridLines> {
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> {
/**
* 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 <code>true</code> if value is in percent, <code>false</code> 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 <code>true</code> if value is in percent, <code>false</code> 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 <code>true</code> if value is in percent, <code>false</code> 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 <code>true</code> if value is in percent, <code>false</code> 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 <code>true</code> if value is in percent, <code>false</code> 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 <code>true</code> if value is in percent, <code>false</code> 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 <a
* href="https://github.com/Actinarium/Rhythm/wiki/Declarative-configuration#inset">the docs</a>
*/
public static class Factory implements RhythmSpecLayerFactory<Inset> {
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);
}
/**
* <p>Create a layer that draws a horizontal or vertical keyline at a specified distance from required edge.</p>
* <p>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.</p>
*/
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 <code>false</code> ({@link #ALIGN_INSIDE}, default) for the keyline to extend towards
* the edge defined by edge affinity, or <code>true</code> ({@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 <a
* href="https://github.com/Actinarium/Rhythm/wiki/Declarative-configuration#keyline">the docs</a>
*/
public static class Factory implements RhythmSpecLayerFactory<Keyline> {
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. <b>Experimental at the
* moment, meaning its behavior, appearance, and parameters may change.</b> 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 <a
* href="https://github.com/Actinarium/Rhythm/wiki/Declarative-configuration#ratio-keyline">the docs</a>
*/
public static class Factory implements RhythmSpecLayerFactory<RatioKeyline> {
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
================================================
<!--
~ 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.actinarium.rhythm.control">
<application
android:allowBackup="true"
android:fullBackupContent="true">
<service
android:name=".RhythmNotificationService"
android:exported="false">
</service>
</application>
</manifest>
================================================
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;
/**
* <p>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}).</p><p><b>Note:</b> if you don’t need the notification or <code>RhythmicFrameLayouts</code>,
* you might actually not need a Rhythm control in your project — just instantiate and use {@link RhythmGroup}s
* directly.</p>
*
* @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<RhythmGroup> 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).
* <br>You may actually set this to <code>null</code> 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
* <code>null</code>) 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();
}
/**
* <p>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).</p> <p><b>Note:</b> 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.</p>
*
* @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 <code>app:rhythmGroup</code> and <code>app:overlayPosition</code> 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 drawab
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
SYMBOL INDEX (306 symbols across 32 files)
FILE: rhythm-control/src/main/java/com/actinarium/rhythm/control/RhythmControl.java
class RhythmControl (line 36) | public final class RhythmControl {
method RhythmControl (line 62) | public RhythmControl(@Nullable Context context) {
method makeGroup (line 74) | public RhythmGroup makeGroup(String title) {
method getGroup (line 95) | public RhythmGroup getGroup(int index) {
method getGroupCount (line 102) | public int getGroupCount() {
method showQuickControl (line 116) | public void showQuickControl(int notificationId) {
method onNotificationDismiss (line 135) | void onNotificationDismiss() {
method requestNotificationUpdate (line 145) | void requestNotificationUpdate() {
method getCurrentNotificationGroup (line 151) | RhythmGroup getCurrentNotificationGroup() {
method selectNextNotificationGroup (line 155) | void selectNextNotificationGroup() {
type Host (line 167) | public interface Host {
method getRhythmControl (line 175) | RhythmControl getRhythmControl();
FILE: rhythm-control/src/main/java/com/actinarium/rhythm/control/RhythmFrameLayout.java
class RhythmFrameLayout (line 45) | public class RhythmFrameLayout extends FrameLayout {
method RhythmFrameLayout (line 93) | public RhythmFrameLayout(Context context) {
method RhythmFrameLayout (line 100) | public RhythmFrameLayout(Context context, AttributeSet attrs) {
method RhythmFrameLayout (line 105) | public RhythmFrameLayout(Context context, AttributeSet attrs, int defS...
method RhythmFrameLayout (line 110) | @TargetApi(Build.VERSION_CODES.LOLLIPOP)
method initFromAttrs (line 116) | private void initFromAttrs(Context context, AttributeSet attrs, int de...
method onLayout (line 141) | @Override
method draw (line 158) | @Override
method onDraw (line 174) | @Override
method dispatchDraw (line 183) | @Override
method verifyDrawable (line 192) | @Override
method getRhythmDrawable (line 202) | public RhythmDrawable getRhythmDrawable() {
method setRhythmDrawable (line 211) | public void setRhythmDrawable(@Nullable RhythmDrawable drawable) {
method getRhythmGroupIndex (line 219) | public int getRhythmGroupIndex() {
method setRhythmGroupIndex (line 228) | public void setRhythmGroupIndex(int rhythmGroupIndex) {
method getOverlayPosition (line 245) | public int getOverlayPosition() {
method setOverlayPosition (line 258) | public void setOverlayPosition(@OverlayPosition int overlayPosition) {
method onRhythmGroupSet (line 269) | private void onRhythmGroupSet() {
method doSetRhythmDrawable (line 289) | private void doSetRhythmDrawable(@Nullable RhythmDrawable drawable) {
FILE: rhythm-control/src/main/java/com/actinarium/rhythm/control/RhythmGroup.java
class RhythmGroup (line 42) | public final class RhythmGroup {
method RhythmGroup (line 63) | public RhythmGroup() {
method setTitle (line 75) | public RhythmGroup setTitle(String title) {
method getTitle (line 85) | public String getTitle() {
method addOverlay (line 95) | public RhythmGroup addOverlay(RhythmOverlay overlay) {
method addOverlays (line 109) | public RhythmGroup addOverlays(Collection<RhythmOverlay> overlays) {
method makeDrawable (line 124) | public RhythmDrawable makeDrawable() {
method decorate (line 139) | @SuppressWarnings("deprecation")
method decorateForeground (line 156) | public void decorateForeground(FrameLayout... views) {
method getCurrentOverlayIndex (line 169) | public int getCurrentOverlayIndex() {
method getOverlayCount (line 177) | public int getOverlayCount() {
method getCurrentOverlay (line 185) | public RhythmOverlay getCurrentOverlay() {
method selectOverlay (line 196) | public void selectOverlay(int index) {
method selectNextOverlay (line 213) | public void selectNextOverlay() {
method toString (line 230) | @Override
method doSetOverlay (line 241) | private void doSetOverlay(RhythmOverlay overlay) {
FILE: rhythm-control/src/main/java/com/actinarium/rhythm/control/RhythmNotificationService.java
class RhythmNotificationService (line 38) | public class RhythmNotificationService extends IntentService {
method RhythmNotificationService (line 50) | public RhythmNotificationService() {
method showNotification (line 61) | static void showNotification(Context context, int notificationId) {
method onHandleIntent (line 68) | @Override
method handleShowNotification (line 85) | private void handleShowNotification(int notificationId) {
method makeCommonNotification (line 145) | private NotificationCompat.Builder makeCommonNotification(String text) {
method handleNextGroup (line 166) | private void handleNextGroup() {
method handleNextOverlay (line 176) | private void handleNextOverlay() {
method handleDismissQuickConfig (line 185) | private void handleDismissQuickConfig() {
class NextGroupRunnable (line 197) | private static class NextGroupRunnable implements Runnable {
method NextGroupRunnable (line 201) | public NextGroupRunnable(RhythmControl control) {
method run (line 205) | @Override
class NextOverlayRunnable (line 214) | private static class NextOverlayRunnable implements Runnable {
method NextOverlayRunnable (line 218) | public NextOverlayRunnable(RhythmGroup group) {
method run (line 222) | @Override
class HideAllOverlaysRunnable (line 231) | private static class HideAllOverlaysRunnable implements Runnable {
method HideAllOverlaysRunnable (line 235) | public HideAllOverlaysRunnable(RhythmControl control) {
method run (line 239) | @Override
FILE: rhythm/src/main/java/com/actinarium/rhythm/AbstractSpecLayerGroup.java
class AbstractSpecLayerGroup (line 32) | public abstract class AbstractSpecLayerGroup<T extends AbstractSpecLayer...
method AbstractSpecLayerGroup (line 37) | public AbstractSpecLayerGroup() {
method AbstractSpecLayerGroup (line 41) | public AbstractSpecLayerGroup(int initialCapacity) {
method draw (line 45) | @Override
method addLayer (line 60) | @SuppressWarnings("unchecked")
method size (line 71) | public int size() {
FILE: rhythm/src/main/java/com/actinarium/rhythm/ArgumentsBundle.java
type ArgumentsBundle (line 35) | public interface ArgumentsBundle {
method hasArgument (line 53) | boolean hasArgument(String key);
method getString (line 61) | String getString(String key);
method getString (line 70) | String getString(String key, @Nullable String defaultValue);
method getInt (line 79) | int getInt(String key, int defaultValue);
method getFloat (line 88) | float getFloat(String key, float defaultValue);
method getBoolean (line 97) | boolean getBoolean(String key, boolean defaultValue);
method getColor (line 106) | @ColorInt
method getGravity (line 118) | int getGravity(String key, int defaultValue);
method getEdgeAffinity (line 130) | @EdgeAffinity
method getDimensionUnits (line 139) | @DimensionUnits
method getDimensionValue (line 151) | float getDimensionValue(String key, float defaultValue);
method getDimensionPixelExact (line 164) | float getDimensionPixelExact(String key, float defaultValue);
method getDimensionPixelOffset (line 177) | int getDimensionPixelOffset(String key, int defaultValue);
method getDimensionPixelSize (line 191) | int getDimensionPixelSize(String key, int defaultValue);
method getDisplayMetrics (line 198) | DisplayMetrics getDisplayMetrics();
FILE: rhythm/src/main/java/com/actinarium/rhythm/MagicVariablesArgumentsBundle.java
class MagicVariablesArgumentsBundle (line 31) | public class MagicVariablesArgumentsBundle extends SimpleArgumentsBundle {
method MagicVariablesArgumentsBundle (line 50) | public MagicVariablesArgumentsBundle(@NonNull Map<String, String> argu...
method hasArgument (line 62) | @Override
method resolveArgument (line 75) | @Override
method equals (line 84) | @Override
method hashCode (line 93) | @Override
FILE: rhythm/src/main/java/com/actinarium/rhythm/RhythmDrawable.java
class RhythmDrawable (line 43) | public class RhythmDrawable extends Drawable {
method RhythmDrawable (line 54) | public RhythmDrawable(@Nullable RhythmOverlay overlay) {
method draw (line 58) | @Override
method getOverlay (line 76) | public RhythmOverlay getOverlay() {
method setOverlay (line 85) | public void setOverlay(@Nullable RhythmOverlay overlay) {
method getDecorated (line 95) | public Drawable getDecorated() {
method setDecorated (line 107) | public void setDecorated(@Nullable Drawable decorated) {
method setAlpha (line 115) | @Override
method setColorFilter (line 123) | @Override
method getOpacity (line 131) | @Override
method isStateful (line 137) | @Override
method setState (line 142) | @Override
method getState (line 147) | @Override
method getPadding (line 152) | @Override
method onStateChange (line 162) | @Override
method onLevelChange (line 167) | @Override
method onBoundsChange (line 172) | @Override
FILE: rhythm/src/main/java/com/actinarium/rhythm/RhythmInflationException.java
class RhythmInflationException (line 27) | public class RhythmInflationException extends RuntimeException {
method RhythmInflationException (line 31) | public RhythmInflationException() {
method RhythmInflationException (line 34) | public RhythmInflationException(String detailMessage) {
method RhythmInflationException (line 38) | public RhythmInflationException(String detailMessage, Throwable throwa...
method RhythmInflationException (line 42) | public RhythmInflationException(Throwable throwable) {
method setLineNumber (line 53) | public RhythmInflationException setLineNumber(@IntRange(from = 0) int ...
method getMessage (line 58) | @Override
FILE: rhythm/src/main/java/com/actinarium/rhythm/RhythmOverlay.java
class RhythmOverlay (line 28) | public class RhythmOverlay extends AbstractSpecLayerGroup<RhythmOverlay> {
method RhythmOverlay (line 35) | public RhythmOverlay() {
method RhythmOverlay (line 44) | public RhythmOverlay(int initialCapacity) {
method setTitle (line 55) | public RhythmOverlay setTitle(String title) {
method getTitle (line 65) | public String getTitle() {
method addLayersFrom (line 78) | public RhythmOverlay addLayersFrom(@NonNull RhythmOverlay source) {
method toString (line 83) | @Override
FILE: rhythm/src/main/java/com/actinarium/rhythm/RhythmOverlayInflater.java
class RhythmOverlayInflater (line 53) | public class RhythmOverlayInflater {
method createDefault (line 101) | public static RhythmOverlayInflater createDefault(Context context) {
method RhythmOverlayInflater (line 125) | public RhythmOverlayInflater(Context context) {
method setMagicVariablesEnabled (line 142) | public RhythmOverlayInflater setMagicVariablesEnabled(boolean enabled) {
method registerFactory (line 155) | public RhythmOverlayInflater registerFactory(@NonNull String layerType...
method addAlias (line 169) | public RhythmOverlayInflater addAlias(@NonNull String existingLayerTyp...
method inflate (line 187) | public List<RhythmOverlay> inflate(@RawRes int rawResId) {
method inflate (line 202) | public List<RhythmOverlay> inflate(String configString) {
method inflate (line 218) | public List<RhythmOverlay> inflate(List<String> configStrings) {
method inflateOverlay (line 288) | @SuppressWarnings("unchecked")
method inflateOverlay (line 302) | public RhythmOverlay inflateOverlay(String configString, @NonNull Map<...
method inflateOverlay (line 313) | @SuppressWarnings("unchecked")
method inflateOverlay (line 326) | public RhythmOverlay inflateOverlay(List<String> configStrings, @NonNu...
method inflateOverlayInternal (line 340) | protected RhythmOverlay inflateOverlayInternal(List<String> configStri...
method inflateLayer (line 458) | public RhythmSpecLayer inflateLayer(String configString, @NonNull Map<...
method inflateLayerInternal (line 469) | protected RhythmSpecLayer inflateLayerInternal(LayerConfig config, int...
method parseConfigInternal (line 502) | protected LayerConfig parseConfigInternal(String configString, @NonNul...
method resolveVariableInternal (line 543) | protected String resolveVariableInternal(@NonNull Map<String, String> ...
method isEmptyOrComment (line 563) | public static boolean isEmptyOrComment(String line) {
class LayerConfig (line 573) | public static class LayerConfig {
method LayerConfig (line 586) | public LayerConfig(@NonNull String layerType, int indent, @NonNull A...
method getLayerType (line 597) | public String getLayerType() {
method getIndent (line 606) | public int getIndent() {
method getArgumentsBundle (line 615) | public ArgumentsBundle getArgumentsBundle() {
FILE: rhythm/src/main/java/com/actinarium/rhythm/RhythmSpecLayer.java
type RhythmSpecLayer (line 29) | public interface RhythmSpecLayer {
method draw (line 39) | void draw(Canvas canvas, Rect drawableBounds);
FILE: rhythm/src/main/java/com/actinarium/rhythm/RhythmSpecLayerFactory.java
type RhythmSpecLayerFactory (line 31) | public interface RhythmSpecLayerFactory<T extends RhythmSpecLayer> {
method getForArguments (line 40) | T getForArguments(ArgumentsBundle argsBundle);
FILE: rhythm/src/main/java/com/actinarium/rhythm/RhythmSpecLayerParent.java
type RhythmSpecLayerParent (line 25) | public interface RhythmSpecLayerParent extends RhythmSpecLayer {
method addLayer (line 34) | RhythmSpecLayerParent addLayer(RhythmSpecLayer layer);
FILE: rhythm/src/main/java/com/actinarium/rhythm/SimpleArgumentsBundle.java
class SimpleArgumentsBundle (line 40) | public class SimpleArgumentsBundle implements ArgumentsBundle {
method SimpleArgumentsBundle (line 59) | public SimpleArgumentsBundle(@NonNull Map<String, String> arguments, @...
method getDisplayMetrics (line 67) | @Override
method hasArgument (line 72) | @Override
method resolveArgument (line 85) | protected String resolveArgument(String key) {
method getString (line 93) | @Override
method getString (line 101) | @Override
method getInt (line 107) | @Override
method getFloat (line 113) | @Override
method getBoolean (line 119) | @Override
method getColor (line 129) | @Override
method getGravity (line 140) | @Override
method getEdgeAffinity (line 181) | @Override
method getDimensionUnits (line 204) | @Override
method getDimensionValue (line 235) | @Override
method getDimensionPixelExact (line 254) | @Override
method getDimensionPixelOffset (line 269) | @Override
method getDimensionPixelSize (line 284) | @Override
method getDimensionPixelRaw (line 309) | public static float getDimensionPixelRaw(float value, @DimensionUnits ...
method equals (line 331) | @Override
method hashCode (line 338) | @Override
FILE: rhythm/src/main/java/com/actinarium/rhythm/SimpleCacheFactory.java
class SimpleCacheFactory (line 27) | public class SimpleCacheFactory<T extends RhythmSpecLayer> implements Rh...
method SimpleCacheFactory (line 32) | public SimpleCacheFactory(RhythmSpecLayerFactory<T> decoratedFactory) {
method getForArguments (line 44) | @Override
FILE: rhythm/src/main/java/com/actinarium/rhythm/internal/ReaderUtils.java
class ReaderUtils (line 35) | public final class ReaderUtils {
method ReaderUtils (line 37) | private ReaderUtils() {}
method readLines (line 46) | public static List<String> readLines(InputStream inputStream) {
method readLines (line 68) | public static List<String> readLines(Context context, @RawRes int rawR...
FILE: rhythm/src/main/java/com/actinarium/rhythm/internal/RuntimeIOException.java
class RuntimeIOException (line 26) | public class RuntimeIOException extends RuntimeException {
method RuntimeIOException (line 28) | public RuntimeIOException() {
method RuntimeIOException (line 32) | public RuntimeIOException(String detailMessage) {
method RuntimeIOException (line 36) | public RuntimeIOException(Throwable throwable) {
method RuntimeIOException (line 40) | public RuntimeIOException(String detailMessage, Throwable throwable) {
FILE: rhythm/src/main/java/com/actinarium/rhythm/layer/Columns.java
class Columns (line 33) | public class Columns extends AbstractSpecLayerGroup<Columns> {
method Columns (line 46) | public Columns(@IntRange(from = 1) int columnCount) {
method Columns (line 58) | public Columns(@IntRange(from = 1) int columnCount, int initialCapacit...
method Columns (line 69) | protected Columns() {
method setColumnCount (line 79) | public Columns setColumnCount(@IntRange(from = 1) int columnCount) {
method draw (line 84) | @Override
class Factory (line 106) | public static class Factory implements RhythmSpecLayerFactory<Columns> {
method getForArguments (line 111) | @Override
FILE: rhythm/src/main/java/com/actinarium/rhythm/layer/DimensionsLabel.java
class DimensionsLabel (line 44) | public class DimensionsLabel implements RhythmSpecLayer {
method DimensionsLabel (line 70) | public DimensionsLabel() {
method setScaleFactor (line 88) | public DimensionsLabel setScaleFactor(@FloatRange(from = 0.0, fromIncl...
method setGravity (line 100) | public DimensionsLabel setGravity(int gravity) {
method setBackgroundColor (line 111) | public DimensionsLabel setBackgroundColor(@ColorInt int color) {
method setTextColor (line 122) | public DimensionsLabel setTextColor(@ColorInt int color) {
method setTextSize (line 133) | public DimensionsLabel setTextSize(@FloatRange(from = 0.0, fromInclusi...
method draw (line 138) | @Override
method prettyPrintDips (line 168) | public static String prettyPrintDips(int px, float scaleFactor) {
class Factory (line 204) | public static class Factory implements RhythmSpecLayerFactory<Dimensio...
method getForArguments (line 212) | @SuppressLint("RtlHardcoded")
FILE: rhythm/src/main/java/com/actinarium/rhythm/layer/Fill.java
class Fill (line 33) | public class Fill implements RhythmSpecLayer {
method Fill (line 42) | public Fill() {
method setColor (line 54) | public Fill setColor(@ColorInt int color) {
method draw (line 59) | @Override
class Factory (line 68) | public static class Factory implements RhythmSpecLayerFactory<Fill> {
method getForArguments (line 73) | @Override
FILE: rhythm/src/main/java/com/actinarium/rhythm/layer/GridLines.java
class GridLines (line 39) | public class GridLines implements RhythmSpecLayer {
method GridLines (line 73) | public GridLines(@ArgumentsBundle.EdgeAffinity int edgeAffinity, @Floa...
method GridLines (line 85) | protected GridLines() {
method setStep (line 97) | public GridLines setStep(@FloatRange(from = 0f, fromInclusive = false)...
method setEdgeAffinity (line 111) | public GridLines setEdgeAffinity(@ArgumentsBundle.EdgeAffinity int edg...
method setColor (line 122) | public GridLines setColor(@ColorInt int color) {
method setThickness (line 133) | public GridLines setThickness(@IntRange(from = 1) int thickness) {
method setLimit (line 145) | public GridLines setLimit(int limit) {
method setOffset (line 159) | public GridLines setOffset(int offset) {
method draw (line 164) | @SuppressLint("RtlHardcoded")
class Factory (line 211) | public static class Factory implements RhythmSpecLayerFactory<GridLine...
method getForArguments (line 221) | @Override
FILE: rhythm/src/main/java/com/actinarium/rhythm/layer/Inset.java
class Inset (line 37) | public class Inset extends AbstractSpecLayerGroup<Inset> {
method Inset (line 95) | public Inset() {
method Inset (line 104) | public Inset(int initialCapacity) {
method setMode (line 117) | public Inset setMode(@Mode int mode) {
method setTop (line 129) | public Inset setTop(int value, boolean isPercent) {
method setBottom (line 143) | public Inset setBottom(int value, boolean isPercent) {
method setLeft (line 157) | public Inset setLeft(int value, boolean isPercent) {
method setRight (line 171) | public Inset setRight(int value, boolean isPercent) {
method setWidth (line 185) | public Inset setWidth(int value, boolean isPercent) {
method setHeight (line 199) | public Inset setHeight(int value, boolean isPercent) {
method draw (line 206) | @Override
method recalculateInsetRect (line 232) | protected void recalculateInsetRect(Rect outerBounds) {
class Factory (line 269) | public static class Factory implements RhythmSpecLayerFactory<Inset> {
method getForArguments (line 281) | @Override
FILE: rhythm/src/main/java/com/actinarium/rhythm/layer/Keyline.java
class Keyline (line 40) | public class Keyline implements RhythmSpecLayer {
method Keyline (line 68) | public Keyline(@ArgumentsBundle.EdgeAffinity int edgeAffinity, int dis...
method Keyline (line 81) | protected Keyline() {
method setDistance (line 92) | public Keyline setDistance(int distance) {
method setEdgeAffinity (line 105) | public Keyline setEdgeAffinity(int edgeAffinity) {
method setColor (line 116) | public Keyline setColor(@ColorInt int color) {
method setThickness (line 129) | public Keyline setThickness(@IntRange(from = 1) int thickness) {
method setAlignOutside (line 145) | public Keyline setAlignOutside(boolean alignOutside) {
method draw (line 150) | @SuppressLint("RtlHardcoded")
class Factory (line 176) | public static class Factory implements RhythmSpecLayerFactory<Keyline> {
method getForArguments (line 185) | @Override
FILE: rhythm/src/main/java/com/actinarium/rhythm/layer/RatioKeyline.java
class RatioKeyline (line 50) | public class RatioKeyline implements RhythmSpecLayer {
method RatioKeyline (line 79) | public RatioKeyline(@IntRange(from = 0) int ratioX, @IntRange(from = 0...
method RatioKeyline (line 87) | protected RatioKeyline(DisplayMetrics metrics) {
method setRatio (line 115) | public RatioKeyline setRatio(@IntRange(from = 0) int ratioX, @IntRange...
method setText (line 127) | public RatioKeyline setText(@Nullable String text) {
method setThickness (line 139) | public RatioKeyline setThickness(@IntRange(from = 1) int thickness) {
method setKeylineColor (line 150) | public RatioKeyline setKeylineColor(@ColorInt int color) {
method setTextColor (line 161) | public RatioKeyline setTextColor(@ColorInt int color) {
method draw (line 166) | @Override
class Factory (line 217) | public static class Factory implements RhythmSpecLayerFactory<RatioKey...
method getForArguments (line 228) | @Override
FILE: sample/src/main/java/com/actinarium/rhythm/sample/MainActivity.java
class MainActivity (line 36) | public class MainActivity extends AppCompatActivity {
method onCreate (line 40) | @SuppressWarnings("deprecation")
method onSaveInstanceState (line 79) | @Override
method onRestoreInstanceState (line 85) | @Override
method setupToolbar (line 94) | private void setupToolbar() {
method setupInteractivity (line 104) | private void setupInteractivity(Bundle savedInstanceState) {
FILE: sample/src/main/java/com/actinarium/rhythm/sample/RhythmSampleApplication.java
class RhythmSampleApplication (line 39) | public class RhythmSampleApplication extends Application implements Rhyt...
method onCreate (line 49) | @Override
method getRhythmControl (line 102) | @Override
method getRhythmOverlayInflater (line 107) | public RhythmOverlayInflater getRhythmOverlayInflater() {
FILE: sample/src/main/java/com/actinarium/rhythm/sample/RhythmSandbox.java
class RhythmSandbox (line 50) | public class RhythmSandbox {
method RhythmSandbox (line 97) | public RhythmSandbox(AppCompatActivity activity, View rootView, Rhythm...
method onSaveInstanceState (line 128) | public void onSaveInstanceState(Bundle outState) {
method onRestoreInstanceState (line 132) | public void onRestoreInstanceState(Bundle savedState) {
method updatePreview (line 146) | private void updatePreview() {
method validate (line 162) | private boolean validate(String overlayConfig) {
class ConfigTokenizer (line 194) | private static class ConfigTokenizer implements MultiAutoCompleteTextV...
method findTokenStart (line 195) | @Override
method findTokenEnd (line 204) | @Override
method terminateToken (line 218) | @Override
class InvalidOverlayDialogFragment (line 232) | public static class InvalidOverlayDialogFragment extends DialogFragment {
method newInstance (line 240) | public static InvalidOverlayDialogFragment newInstance(String error) {
method onAttach (line 248) | @Override
method onCreateDialog (line 254) | @Override
FILE: sample/src/main/java/com/actinarium/rhythm/sample/customlayers/ImageBox.java
class ImageBox (line 39) | public class ImageBox implements RhythmSpecLayer {
method ImageBox (line 52) | public ImageBox(int width, int height, int distanceX, int distanceY, i...
method ImageBox (line 76) | private ImageBox(float scaleFactor) {
method draw (line 90) | @Override
class Factory (line 106) | public static class Factory implements RhythmSpecLayerFactory<ImageBox> {
method getForArguments (line 110) | @Override
FILE: sample/src/main/java/com/actinarium/rhythm/sample/customlayers/LayoutBounds.java
class LayoutBounds (line 35) | public class LayoutBounds implements RhythmSpecLayer {
method LayoutBounds (line 41) | public LayoutBounds(int crosshairSize) {
method LayoutBounds (line 46) | private LayoutBounds() {
method draw (line 52) | @Override
class Factory (line 75) | public static class Factory implements RhythmSpecLayerFactory<LayoutBo...
method getForArguments (line 79) | @Override
FILE: sample/src/main/java/com/actinarium/rhythm/sample/util/BulletSpan.java
class BulletSpan (line 33) | public class BulletSpan implements LeadingMarginSpan {
method BulletSpan (line 42) | public BulletSpan(int bulletRadius, int bulletCenterX, int leadingMarg...
method BulletSpan (line 50) | public BulletSpan(Parcel src) {
method describeContents (line 58) | public int describeContents() {
method writeToParcel (line 62) | public void writeToParcel(Parcel dest, int flags) {
method getLeadingMargin (line 70) | public int getLeadingMargin(boolean first) {
method drawLeadingMargin (line 74) | public void drawLeadingMargin(Canvas c, Paint p, int x, int dir,
FILE: sample/src/main/java/com/actinarium/rhythm/sample/util/ViewUtils.java
class ViewUtils (line 24) | public final class ViewUtils {
method ViewUtils (line 26) | private ViewUtils() {}
method makeBulletList (line 28) | public static CharSequence makeBulletList(int bulletRadius, int bullet...
Condensed preview — 64 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (308K chars).
[
{
"path": ".gitignore",
"chars": 65,
"preview": ".gradle\n/local.properties\n.DS_Store\n/build\n/captures\n*.iml\n.idea\n"
},
{
"path": "LICENSE",
"chars": 11357,
"preview": "\n Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.md",
"chars": 13219,
"preview": "# Rhythm \n[](#discontinued-library) [![Downloa"
},
{
"path": "build.gradle",
"chars": 1113,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "gradle/wrapper/gradle-wrapper.properties",
"chars": 815,
"preview": "#\n# Copyright (C) 2016 Actinarium\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use "
},
{
"path": "gradle.properties",
"chars": 1438,
"preview": "#\n# Copyright (C) 2016 Actinarium\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use "
},
{
"path": "gradlew",
"chars": 5080,
"preview": "#!/usr/bin/env bash\n\n##############################################################################\n##\n## Gradle start "
},
{
"path": "gradlew.bat",
"chars": 2314,
"preview": "@if \"%DEBUG%\" == \"\" @echo off\n@rem ##########################################################################\n@rem\n@rem "
},
{
"path": "rhythm/.gitignore",
"chars": 12,
"preview": "/build\n*.iml"
},
{
"path": "rhythm/build.gradle",
"chars": 3529,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "rhythm/proguard-rules.pro",
"chars": 673,
"preview": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in C:"
},
{
"path": "rhythm/src/main/AndroidManifest.xml",
"chars": 801,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright (C) 2016 Actinarium\n ~\n ~ Licensed under the Apache License,"
},
{
"path": "rhythm/src/main/java/com/actinarium/rhythm/AbstractSpecLayerGroup.java",
"chars": 2329,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "rhythm/src/main/java/com/actinarium/rhythm/ArgumentsBundle.java",
"chars": 7727,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "rhythm/src/main/java/com/actinarium/rhythm/MagicVariablesArgumentsBundle.java",
"chars": 4332,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "rhythm/src/main/java/com/actinarium/rhythm/RhythmDrawable.java",
"chars": 6479,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "rhythm/src/main/java/com/actinarium/rhythm/RhythmInflationException.java",
"chars": 1942,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "rhythm/src/main/java/com/actinarium/rhythm/RhythmOverlay.java",
"chars": 2877,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "rhythm/src/main/java/com/actinarium/rhythm/RhythmOverlayInflater.java",
"chars": 29953,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "rhythm/src/main/java/com/actinarium/rhythm/RhythmSpecLayer.java",
"chars": 1884,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "rhythm/src/main/java/com/actinarium/rhythm/RhythmSpecLayerFactory.java",
"chars": 1872,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "rhythm/src/main/java/com/actinarium/rhythm/RhythmSpecLayerParent.java",
"chars": 1261,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "rhythm/src/main/java/com/actinarium/rhythm/SimpleArgumentsBundle.java",
"chars": 12940,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "rhythm/src/main/java/com/actinarium/rhythm/SimpleCacheFactory.java",
"chars": 1824,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "rhythm/src/main/java/com/actinarium/rhythm/internal/ReaderUtils.java",
"chars": 2704,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "rhythm/src/main/java/com/actinarium/rhythm/internal/RuntimeIOException.java",
"chars": 1236,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "rhythm/src/main/java/com/actinarium/rhythm/layer/Columns.java",
"chars": 4405,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "rhythm/src/main/java/com/actinarium/rhythm/layer/DimensionsLabel.java",
"chars": 8986,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "rhythm/src/main/java/com/actinarium/rhythm/layer/Fill.java",
"chars": 2497,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "rhythm/src/main/java/com/actinarium/rhythm/layer/GridLines.java",
"chars": 10828,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "rhythm/src/main/java/com/actinarium/rhythm/layer/Inset.java",
"chars": 12735,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "rhythm/src/main/java/com/actinarium/rhythm/layer/Keyline.java",
"chars": 9212,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "rhythm/src/main/java/com/actinarium/rhythm/layer/RatioKeyline.java",
"chars": 9877,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "rhythm-control/.gitignore",
"chars": 7,
"preview": "/build\n"
},
{
"path": "rhythm-control/build.gradle",
"chars": 3597,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "rhythm-control/proguard-rules.pro",
"chars": 673,
"preview": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in C:"
},
{
"path": "rhythm-control/src/main/AndroidManifest.xml",
"chars": 1010,
"preview": "<!--\n ~ Copyright (C) 2016 Actinarium\n ~\n ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n ~ you m"
},
{
"path": "rhythm-control/src/main/java/com/actinarium/rhythm/control/RhythmControl.java",
"chars": 7003,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "rhythm-control/src/main/java/com/actinarium/rhythm/control/RhythmFrameLayout.java",
"chars": 11607,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "rhythm-control/src/main/java/com/actinarium/rhythm/control/RhythmGroup.java",
"chars": 9552,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "rhythm-control/src/main/java/com/actinarium/rhythm/control/RhythmNotificationService.java",
"chars": 10304,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "rhythm-control/src/main/res/values/attrs.xml",
"chars": 1211,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright (C) 2016 Actinarium\n ~\n ~ Licensed under the Apache License,"
},
{
"path": "rhythm-control/src/main/res/values/strings.xml",
"chars": 1401,
"preview": "<!--\n ~ Copyright (C) 2016 Actinarium\n ~\n ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n ~ you m"
},
{
"path": "sample/.gitignore",
"chars": 11,
"preview": "/build\n*iml"
},
{
"path": "sample/build.gradle",
"chars": 1399,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "sample/proguard-rules.pro",
"chars": 673,
"preview": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in C:"
},
{
"path": "sample/src/main/AndroidManifest.xml",
"chars": 1548,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright (C) 2016 Actinarium\n ~\n ~ Licensed under the Apache License,"
},
{
"path": "sample/src/main/java/com/actinarium/rhythm/sample/MainActivity.java",
"chars": 6230,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "sample/src/main/java/com/actinarium/rhythm/sample/RhythmSampleApplication.java",
"chars": 5242,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "sample/src/main/java/com/actinarium/rhythm/sample/RhythmSandbox.java",
"chars": 10208,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "sample/src/main/java/com/actinarium/rhythm/sample/customlayers/ImageBox.java",
"chars": 4732,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "sample/src/main/java/com/actinarium/rhythm/sample/customlayers/LayoutBounds.java",
"chars": 3362,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "sample/src/main/java/com/actinarium/rhythm/sample/util/BulletSpan.java",
"chars": 3450,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "sample/src/main/java/com/actinarium/rhythm/sample/util/ViewUtils.java",
"chars": 1505,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
},
{
"path": "sample/src/main/res/layout/activity_main.xml",
"chars": 9980,
"preview": "<!--\n ~ Copyright (C) 2016 Actinarium\n ~\n ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n ~ you m"
},
{
"path": "sample/src/main/res/layout/overlay_sandbox.xml",
"chars": 4564,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright (C) 2016 Actinarium\n ~\n ~ Licensed under the Apache License,"
},
{
"path": "sample/src/main/res/layout-land/overlay_sandbox.xml",
"chars": 5027,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright (C) 2016 Actinarium\n ~\n ~ Licensed under the Apache License,"
},
{
"path": "sample/src/main/res/raw/overlay_config",
"chars": 4687,
"preview": "// This is a comment. Comments start with two slashes //\n// End of line comments are not supported\n\n// -------- GLOBAL V"
},
{
"path": "sample/src/main/res/values/dimens.xml",
"chars": 864,
"preview": "<!--\n ~ Copyright (C) 2016 Actinarium\n ~\n ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n ~ you m"
},
{
"path": "sample/src/main/res/values/strings.xml",
"chars": 3789,
"preview": "<!--\n ~ Copyright (C) 2016 Actinarium\n ~\n ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n ~ you m"
},
{
"path": "sample/src/main/res/values/styles.xml",
"chars": 1361,
"preview": "<!--\n ~ Copyright (C) 2016 Actinarium\n ~\n ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n ~ you m"
},
{
"path": "settings.gradle",
"chars": 647,
"preview": "/*\n * Copyright (C) 2016 Actinarium\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not"
}
]
// ... and 2 more files (download for full content)
About this extraction
This page contains the full source code of the Actinarium/Rhythm GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 64 files (287.1 KB), approximately 66.8k tokens, and a symbol index with 306 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.