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);
Listnull.
*
* @param key argument key
* @return true if argument is present regardless of value
*/
boolean hasArgument(String key);
/**
* Get argument value as a string.
*
* @param key argument key
* @return argument value as a string, or null if the argument has null value or it cannot be retrieved
*/
String getString(String key);
/**
* Get argument value as a string with fallback to default value if argument is missing.
*
* @param key argument key
* @param defaultValue fallback value
* @return argument value as raw string
*/
String getString(String key, @Nullable String defaultValue);
/**
* Get argument value as integer with fallback to default value if argument is missing.
*
* @param key argument key
* @param defaultValue fallback value
* @return argument value parsed as integer
*/
int getInt(String key, int defaultValue);
/**
* Get argument value as float with fallback to default value if argument is missing.
*
* @param key argument key
* @param defaultValue fallback value
* @return argument value parsed as float
*/
float getFloat(String key, float defaultValue);
/**
* Get boolean argument. Arguments specified as arg are identical to arg=true.
*
* @param key argument key
* @param defaultValue value if argument is not present
* @return argument boolean value
*/
boolean getBoolean(String key, boolean defaultValue);
/**
* Get argument value as color integer with fallback to default value if argument is missing.
*
* @param key argument key
* @param defaultValue fallback value
* @return argument value parsed as color integer
*/
@ColorInt
int getColor(String key, @ColorInt int defaultValue);
/**
* Get argument value as a gravity value (a combination of {@link Gravity} constants) with fallback to default value
* if argument is missing.
*
* @param key argument key
* @param defaultValue fallback value
* @return gravity constant
* @see #getEdgeAffinity(String, int)
*/
int getGravity(String key, int defaultValue);
/**
* Get argument as an {@link EdgeAffinity} constant, which can be either {@link Gravity#TOP}, {@link Gravity#LEFT},
* {@link Gravity#RIGHT}, or {@link Gravity#BOTTOM}, with fallback to default value if argument is missing or
* invalid.
*
* @param key argument key
* @param defaultValue fallback value
* @return gravity constant
* @see #getGravity(String, int)
*/
@EdgeAffinity
int getEdgeAffinity(String key, @EdgeAffinity int defaultValue);
/**
* Get the units of a dimension argument.
*
* @param key argument key
* @return dimension argument units, or {@link #UNITS_NULL} if the argument is null or missing
*/
@DimensionUnits
int getDimensionUnits(String key);
/**
* Get raw numeric value from dimension argument disregarding units and NOT performing any conversion to pixels.
*
* @param key argument key
* @param defaultValue fallback value in pixels
* @return dimension argument raw value
* @see #getDimensionPixelSize(String, int)
* @see #getDimensionPixelOffset(String, int)
*/
float getDimensionValue(String key, float defaultValue);
/**
* Get dimension argument value as pixels with possible fallback to default value if argument is missing or invalid.
* Unlike {@link #getDimensionPixelSize(String, int)} and {@link #getDimensionPixelSize(String, int)}, this method
* doesn't perform any rounding.
*
* @param key argument key
* @param defaultValue fallback value in pixels
* @return argument value converted to pixels
* @see #getDimensionPixelSize(String, int)
* @see #getDimensionPixelOffset(String, int)
*/
float getDimensionPixelExact(String key, float defaultValue);
/**
* Get dimension argument value as pixels with possible fallback to default value if argument is missing or invalid.
* Unlike {@link #getDimensionPixelSize(String, int)}, this method is expected to round the raw value down to
* the closest integer.
*
* @param key argument key
* @param defaultValue fallback value in pixels
* @return argument value converted to pixels
* @see #getDimensionPixelSize(String, int)
* @see #getDimensionPixelExact(String, float)
*/
int getDimensionPixelOffset(String key, int defaultValue);
/**
* Get dimension argument value as pixels with possible fallback to default value if argument is missing or invalid.
* Unlike {@link #getDimensionPixelOffset(String, int)}}, this method is expected to round the raw value up or
* down to the closest integer by common rules, and must ensure the result is at least 1px if original value is
* not 0.
*
* @param key argument key
* @param defaultValue fallback value in pixels
* @return argument value converted to pixels
* @see #getDimensionPixelOffset(String, int)
* @see #getDimensionPixelExact(String, float)
*/
int getDimensionPixelSize(String key, int defaultValue);
/**
* Get display metrics associated with this arguments bundle.
*
* @return display metrics object
*/
DisplayMetrics getDisplayMetrics();
/**
* Type definition for dimension argument units
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({UNITS_NULL, UNITS_NUMBER, UNITS_PERCENT, UNITS_PX, UNITS_DP, UNITS_SP, UNITS_MM, UNITS_PT, UNITS_IN})
public @interface DimensionUnits {
}
/**
* Type definition for screen edge that a keyline or pattern must be attached to. Used by some layers
*/
@SuppressLint("RtlHardcoded")
@Retention(RetentionPolicy.SOURCE)
@IntDef({Gravity.TOP, Gravity.BOTTOM, Gravity.LEFT, Gravity.RIGHT, Gravity.NO_GRAVITY})
@interface EdgeAffinity {
}
}
================================================
FILE: rhythm/src/main/java/com/actinarium/rhythm/MagicVariablesArgumentsBundle.java
================================================
/*
* Copyright (C) 2016 Actinarium
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.actinarium.rhythm;
import android.support.annotation.NonNull;
import android.util.DisplayMetrics;
import java.util.Map;
/**
* An implementation of {@link ArgumentsBundle} that utilizes “magic variables” mechanism to resolve missing
* layer arguments: if an argument is not explicitly specified, it tries resolving it from a variable named in a special
* pattern @{layer_name}_{arg_name} where dashes are replaced with underscores.
*
* @author Paul Danyliuk
*/
public class MagicVariablesArgumentsBundle extends SimpleArgumentsBundle {
protected String mLayerNamePrefix;
protected Mapgrid-lines
* @param metrics Display metrics associated with this arguments bundle, required so that dimension values (dp,
* sp
*/
public MagicVariablesArgumentsBundle(@NonNull Map@{layer_name}_{arg_name} (concatenated layer and
* argument names with dashes replaced by underscores).
*
* @param key key of the argument whose value to resolve
* @return string representation of resolved value
*/
@Override
protected String resolveArgument(String key) {
String value = mArguments.get(key);
if (value == null && !mArguments.containsKey(key)) {
value = mVariables.get(mLayerNamePrefix + key.replace('-', '_'));
}
return value;
}
@Override
public boolean equals(Object o) {
if (this == o) { return true; }
if (o == null || getClass() != o.getClass()) { return false; }
if (!super.equals(o)) { return false; }
MagicVariablesArgumentsBundle that = (MagicVariablesArgumentsBundle) o;
return mVariables.equals(that.mVariables);
}
@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + mVariables.hashCode();
return result;
}
}
================================================
FILE: rhythm/src/main/java/com/actinarium/rhythm/RhythmDrawable.java
================================================
/*
* Copyright (C) 2016 Actinarium
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.actinarium.rhythm;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
/**
* Renders the currently assigned {@link RhythmOverlay} and serves as an adapter between Rhythm (which sets the * overlay to draw by this drawable at the moment) and the views where the overlay (grids, keylines etc) must be * applied. You can use it as any other {@link Drawable} from Android SDK, e.g. assign it as background, foreground, * overlay etc, but keep in mind that for different views, separate drawable instances must be created.
For easy
* integration with existing layouts, RhythmDrawable can decorate another Drawable —
* that is, draw the decorated one below and then the overlay atop. This can be especially useful when decorating the
* views that already have backgrounds. Note: as of this version, decoration logic is very limited for the sake
* of simplicity, therefore in some cases (e.g. when decorated drawable is a state list or a level list), it may not
* respond correctly to state and level changes (e.g. pressing a decorated button won’t highlight it). But since
* decoration is mostly intended for ViewGroups, it’s unlikely that this should be an issue under normal use.
Normally you shouldn’t extend this class. If you need to perform custom drawing, consider creating a custom {@link * RhythmSpecLayer} implementation instead.
* * @author Paul Danyliuk */ public class RhythmDrawable extends Drawable { protected RhythmOverlay mOverlay; protected Drawable mDecorated; /** * Create a Rhythm drawable for given Rhythm overlay. You can then change the displayed overlay via {@link * #setOverlay(RhythmOverlay)} method. * * @param overlay Rhythm overlay to render into this drawable, can benull.
*/
public RhythmDrawable(@Nullable RhythmOverlay overlay) {
mOverlay = overlay;
}
@Override
public void draw(Canvas canvas) {
// Draw decorated drawable if present
if (mDecorated != null) {
mDecorated.draw(canvas);
}
// Draw overlay if present
if (mOverlay != null) {
mOverlay.draw(canvas, getBounds());
}
}
/**
* Get current overlay
*
* @return Currently active Rhythm overlay, or null if no overlay is set
*/
public RhythmOverlay getOverlay() {
return mOverlay;
}
/**
* Set a {@link RhythmOverlay} for this drawable. Will request redraw of this drawable’s view.
*
* @param overlay Overlay to draw. Provide null to disable overlay.
*/
public void setOverlay(@Nullable RhythmOverlay overlay) {
mOverlay = overlay;
invalidateSelf();
}
/**
* Get decorated drawable (the one drawn under the overlay) if present
*
* @return Decorated drawable or null
*/
public Drawable getDecorated() {
return mDecorated;
}
/**
* Set a {@link Drawable} to decorate. Should be used when decorating an existing background or foreground of a view
* — this way the original drawable will be preserved and the overlay will be drawn atop. Note: to
* function properly, the decorated drawable’s {@link Drawable#setCallback(Callback) callbacks} must be set. Also
* see {@link RhythmDrawable the class’ description} for more info on decoration support.
*
* @param decorated A drawable to draw below the overlay. Set null to remove decorated drawable.
*/
public void setDecorated(@Nullable Drawable decorated) {
mDecorated = decorated;
if (mDecorated != null) {
mDecorated.setBounds(getBounds());
}
invalidateSelf();
}
@Override
public void setAlpha(int alpha) {
// No-op for the overlay for simplicity reasons - propagate to decorated drawable only
if (mDecorated != null) {
mDecorated.setAlpha(alpha);
}
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
// No-op for the overlay for simplicity reasons - propagate to decorated drawable only
if (mDecorated != null) {
mDecorated.setColorFilter(colorFilter);
}
}
@Override
public int getOpacity() {
final int overlayOpacity = mOverlay == null ? PixelFormat.TRANSPARENT : PixelFormat.TRANSLUCENT;
return mDecorated != null ? Drawable.resolveOpacity(mDecorated.getOpacity(), overlayOpacity) : overlayOpacity;
}
@Override
public boolean isStateful() {
return mDecorated != null && mDecorated.isStateful();
}
@Override
public boolean setState(int[] stateSet) {
return mDecorated != null && mDecorated.setState(stateSet);
}
@Override
public int[] getState() {
return mDecorated != null ? mDecorated.getState() : super.getState();
}
@Override
public boolean getPadding(@NonNull Rect padding) {
if (mDecorated == null) {
padding.set(0, 0, 0, 0);
return false;
} else {
return mDecorated.getPadding(padding);
}
}
@Override
protected boolean onStateChange(int[] state) {
return mDecorated != null && mDecorated.setState(state);
}
@Override
protected boolean onLevelChange(int level) {
return mDecorated != null && mDecorated.setLevel(level);
}
@Override
protected void onBoundsChange(Rect bounds) {
if (mDecorated != null) {
mDecorated.setBounds(bounds);
}
}
}
================================================
FILE: rhythm/src/main/java/com/actinarium/rhythm/RhythmInflationException.java
================================================
/*
* Copyright (C) 2016 Actinarium
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.actinarium.rhythm;
import android.support.annotation.IntRange;
/**
* A runtime exception to be thrown when there is an error inflating declarative configuration, usually because of
* syntax error or violated argument value constraints.
*
* @author Paul Danyliuk
*/
public class RhythmInflationException extends RuntimeException {
private int mLineNumber = 0;
public RhythmInflationException() {
}
public RhythmInflationException(String detailMessage) {
super(detailMessage);
}
public RhythmInflationException(String detailMessage, Throwable throwable) {
super(detailMessage, throwable);
}
public RhythmInflationException(Throwable throwable) {
super(throwable);
}
/**
* Set the index of the line (0-based) where the error happened. If set, text "Line {x+1}: " will be prepended to
* error message
*
* @param index index of the line where error happened, zero-based
* @return this for chaining
*/
public RhythmInflationException setLineNumber(@IntRange(from = 0) int index) {
mLineNumber = index + 1;
return this;
}
@Override
public String getMessage() {
String message = super.getMessage();
return mLineNumber == 0 ? message : "Line " + mLineNumber + ": " + message;
}
}
================================================
FILE: rhythm/src/main/java/com/actinarium/rhythm/RhythmOverlay.java
================================================
/*
* Copyright (C) 2016 Actinarium
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.actinarium.rhythm;
import android.support.annotation.NonNull;
/**
* Defines a single overlay configuration, i.e. which spec layers (grid lines, keylines etc) must be drawn in the {@link
* RhythmDrawable}(s) where this overlay is currently set. Composed of granular {@link RhythmSpecLayer}s, which hold
* their own configuration (see descriptions of respectable implementations) and are drawn in the order of adding.
*
* @author Paul Danyliuk
*/
public class RhythmOverlay extends AbstractSpecLayerGroupnull if the overlay is anonymous
*/
public String getTitle() {
return mTitle;
}
/**
* Add all layers to this overlay from another. Convenient if you have a common set of layers that you wish to * include in multiple overlays, or want to create an overlay that combines a few others.
Warning: for * simplicity and performance reasons the same layer objects are used, therefore it’s strongly advised that you * don’t mutate them after adding.
* * @param source Existing overlay to add all layers from * @return this for chaining */ public RhythmOverlay addLayersFrom(@NonNull RhythmOverlay source) { mLayers.addAll(source.mLayers); return this; } @Override public String toString() { return mTitle != null ? mTitle : "Untitled overlay@" + Integer.toHexString(hashCode()); } } ================================================ FILE: rhythm/src/main/java/com/actinarium/rhythm/RhythmOverlayInflater.java ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.actinarium.rhythm; import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.RawRes; import android.util.DisplayMetrics; import com.actinarium.rhythm.internal.ReaderUtils; import com.actinarium.rhythm.layer.Columns; import com.actinarium.rhythm.layer.DimensionsLabel; import com.actinarium.rhythm.layer.Fill; import com.actinarium.rhythm.layer.GridLines; import com.actinarium.rhythm.layer.Inset; import com.actinarium.rhythm.layer.Keyline; import com.actinarium.rhythm.layer.RatioKeyline; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** *A default inflater that creates {@linkplain RhythmOverlay}s from text configuration using registered layer * factories. Supports inflating multiple overlays from configuration files (see the docs) separated by newlines as well as separate overlays; * supports comments and variables, and supporting custom spec layers by allowing to register spec layer * factories.
The provided implementation is a reference one — developers are welcome to subclass this * inflater or any classes of the inflation pipeline to override certain aspects, or implement their own inflation * mechanisms (e.g. different lexers, parse-time validation, transformations, XML/JSON/YAML support etc) entirely from * scratch should they need something different.
* * @author Paul Danyliuk */ public class RhythmOverlayInflater { /** * Initial capacity of {layer type} -> {factory} map. */ private static final int INITIAL_FACTORIES_CAPACITY = 16; /** * A regex to search for arguments in configuration string by a following template: key[=value] */ protected static final Pattern PATTERN_ARGUMENTS = Pattern.compile("([^\\s=]+)(?:=([^\\s]+))?"); /** * A regex to validate and parse variables in configuration string by a following template: @variable=value */ protected static final Pattern PATTERN_VARIABLES = Pattern.compile("(@[\\w]+)=(.*)"); /** * Used internally to indicate that there's no overlay block started at the moment of evaluating current line */ private static final int NOT_STARTED = -1; protected Context mContext; protected DisplayMetrics mDisplayMetrics; protected MapCreate a new instance of default overlay inflater. It comes pre-configured to inflate all bundled {@link * RhythmSpecLayer} types, and you can add custom factories for your custom spec layers.
By default, {@link * GridLines}, {@link Keyline}, {@link RatioKeyline}, and {@link Fill} layer instances are cached and reused for the * same configuration lines — if you don't want this behavior (e.g. if you want to mutate the inflated layers * individually afterwards), create an empty inflater and register the factories yourself like this:
*
* RhythmOverlayInflater inflater = new RhythmOverlayInflater(context);
* inflater.registerFactory(GridLines.Factory.LAYER_TYPE, new GridLines.Factory());
* inflater.registerFactory(Keyline.Factory.LAYER_TYPE, new Keyline.Factory());
* inflater.registerFactory(RatioKeyline.Factory.LAYER_TYPE, new RatioKeyline.Factory());
* inflater.registerFactory(Fill.Factory.LAYER_TYPE, new Fill.Factory());
* inflater.registerFactory(Inset.Factory.LAYER_TYPE, new Inset.Factory());
* inflater.registerFactory(Columns.Factory.LAYER_TYPE, new Columns.Factory());
* inflater.registerFactory(DimensionsLabel.Factory.LAYER_TYPE, new DimensionsLabel.Factory());
*
*
* @param context Context
* @return a new overlay inflater instance configured to inflate bundled spec layers
* @see #RhythmOverlayInflater(Context)
*/
public static RhythmOverlayInflater createDefault(Context context) {
final RhythmOverlayInflater inflater = new RhythmOverlayInflater(context);
// Register bundled spec layers. Wrap keyline, fill, grid, and ratio keyline factories in caching decorators
inflater.mFactories.put(GridLines.Factory.LAYER_TYPE, new SimpleCacheFactory<>(new GridLines.Factory()));
inflater.mFactories.put(Keyline.Factory.LAYER_TYPE, new SimpleCacheFactory<>(new Keyline.Factory()));
inflater.mFactories.put(RatioKeyline.Factory.LAYER_TYPE, new SimpleCacheFactory<>(new RatioKeyline.Factory()));
inflater.mFactories.put(Fill.Factory.LAYER_TYPE, new SimpleCacheFactory<>(new Fill.Factory()));
inflater.mFactories.put(Inset.Factory.LAYER_TYPE, new Inset.Factory());
inflater.mFactories.put(Columns.Factory.LAYER_TYPE, new Columns.Factory());
inflater.mFactories.put(DimensionsLabel.Factory.LAYER_TYPE, new DimensionsLabel.Factory());
return inflater;
}
/**
* Create a new instance of overlay inflater with no factories registered. Call this constructor only if you need a
* blank inflater that you are going to configure from scratch (i.e. by registering all the required factories with
* {@link #registerFactory(String, RhythmSpecLayerFactory)}). If you need an inflater with all bundled spec layers
* pre-configured, use {@link #createDefault(Context)} instead.
*
* @param context Context
* @see #createDefault(Context)
*/
public RhythmOverlayInflater(Context context) {
mContext = context.getApplicationContext();
mDisplayMetrics = mContext.getResources().getDisplayMetrics();
mFactories = new HashMap<>(INITIAL_FACTORIES_CAPACITY);
}
/**
* Enable or disable “magic variables” support in this inflater instance. Enabling this will allow to
* specify default arguments for spec layers by defining global and local variables named with the following
* pattern: @{layer_name}_{arg_name}, where dashes are replaced with underscores.
Warning:“magic variables” is an experimental feature and therefore disabled by default. * Normally it shouldn't have significant impact on performance, yet it's advised to only enable it if using magic * variables is really desirable over explicitly setting values to spec layers individually.
* * @param enabled true to enable magic variables support, false to disable it. * @return this for chaining */ public RhythmOverlayInflater setMagicVariablesEnabled(boolean enabled) { mAreMagicVariablesEnabled = enabled; return this; } /** * Register a factory for provided layer type. Use this method to register factories for your custom spec layers or * override default behavior. You can add the same factory for multiple layer types, e.g. for aliasing. * * @param layerType string that identifies a specific spec layer class; the first argument in each config line * @param factory a factory object that will inflate config line into a layer * @return this for chaining */ public RhythmOverlayInflater registerFactory(@NonNull String layerType, @NonNull RhythmSpecLayerFactory factory) { mFactories.put(layerType, factory); return this; } /** * Add an alias for arbitrary layer type. This will make multiple layer type strings map to the same factory. For * custom layers, a slightly more efficient way would be to simply call {@link #registerFactory(String, * RhythmSpecLayerFactory)} multiple times with different strings and the same factory objects to avoid lookups. * * @param existingLayerType layer type string for layer to alias (used for lookup) * @param aliasLayerType layer type string to map to the same factory * @return this for chaining */ public RhythmOverlayInflater addAlias(@NonNull String existingLayerType, @NonNull String aliasLayerType) { RhythmSpecLayerFactory factory = mFactories.get(existingLayerType); if (factory != null) { mFactories.put(aliasLayerType, factory); } else { throw new IllegalArgumentException("No factory registered for type \"" + existingLayerType + "\""); } return this; } /** * Inflate a Rhythm configuration file into a list of {@link RhythmOverlay RhythmOverlays}, which you can then * assign to a group, or make sub-lists of and assign to different groups. * * @param rawResId Raw configuration file with syntax according to the docs * @return A list of inflated Rhythm overlays * @see #inflate(List) */ public ListSame as {@link #inflate(int)}, but accepts the configuration file already split in lines as strings.
*This method walks over the lines and determines how the config should be split into separate overlays.
* * @param configStrings Configuration file split as separate lines. Must follow the same syntax rules as the * configuration file, that is, nonull strings, and overlays being separated by
* an empty line
* @return A list of inflated Rhythm overlays
* @see #inflate(int)
* @see #inflate(String)
*/
public List@primary=#FF0000 to use in color=@primary). Cannot be
* null — pass {@link Collections#EMPTY_MAP} if there are no variables.
* @return inflated layer
*/
public RhythmSpecLayer inflateLayer(String configString, @NonNull Map@primary=#FF0000 to use in color=@primary)
* @param lineNumber Line number to report in case of error
* @return layer config object with layer configuration and metadata
*/
protected LayerConfig parseConfigInternal(String configString, @NonNull Map//) and thus
* should be ignored.
*
* @param line line to test, should be pre-trimmed
* @return true if empty or comment
*/
public static boolean isEmptyOrComment(String line) {
return line.length() == 0 || (line.charAt(0) == '/' && line.length() >= 2 && line.charAt(1) == '/');
}
/**
* A spec layer descriptor holding arguments and metadata, used internally by {@link RhythmOverlayInflater} to carry
* values needed to inflate individual layers and their hierarchies.
*
* @author Paul Danyliuk
*/
public static class LayerConfig {
protected String mLayerType;
protected int mIndent;
protected ArgumentsBundle mArgumentsBundle;
/**
* Create layer config object for layer of given type, with known indent, and with pre-filled arguments bag
*
* @param layerType spec layer type, used for appropriate factory lookup
* @param indent number of leading spaces in the config line, used to resolve layer hierarchy
* @param argumentsBundle an object describing parsed layer configuration (arguments and values)
*/
public LayerConfig(@NonNull String layerType, int indent, @NonNull ArgumentsBundle argumentsBundle) {
mLayerType = layerType;
mIndent = indent;
mArgumentsBundle = argumentsBundle;
}
/**
* Get the name of {@link RhythmSpecLayer spec layer} to inflate with these arguments
*
* @return spec layer type
*/
public String getLayerType() {
return mLayerType;
}
/**
* Get the number of spaces this config line was indented with. Used internally to resolve grouping
*
* @return number of spaces
*/
public int getIndent() {
return mIndent;
}
/**
* Get the configuration of this layer presented by an {@link ArgumentsBundle} object
*
* @return layer configuration bundle
*/
public ArgumentsBundle getArgumentsBundle() {
return mArgumentsBundle;
}
}
}
================================================
FILE: rhythm/src/main/java/com/actinarium/rhythm/RhythmSpecLayer.java
================================================
/*
* Copyright (C) 2016 Actinarium
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.actinarium.rhythm;
import android.graphics.Canvas;
import android.graphics.Rect;
/**
* Spec layer is a descriptor of a granular piece of overlay (e.g. a single line, a repeating line etc), which both * holds the configuration of its appearance (hence the spec) and is also capable of drawing itself onto the provided * canvas (hence the layer). Unlike Drawables, where separate instances are required each time they are used, spec layer * instances are created per configuration and can be reused across many {@link RhythmDrawable}s (views, * overlays).
You can create custom spec layers by implementing this interface.
*/ public interface RhythmSpecLayer { /** * Draw itself to the provided canvas within provided bounds according to internal configuration (if any) * * @param canvas Canvas for the layer to draw itself to * @param drawableBounds Bounds where this layer should draw itself. Since these are the bounds of a {@link * RhythmDrawable} connected to the view, they are usually the same as the view’s bounds, so * you can use this parameter to get the view’s dimensions should you need them. */ void draw(Canvas canvas, Rect drawableBounds); } ================================================ FILE: rhythm/src/main/java/com/actinarium/rhythm/RhythmSpecLayerFactory.java ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.actinarium.rhythm; /** *Interface for a factory that can instantiate a {@link RhythmSpecLayer} implementation from provided {@link
* ArgumentsBundle}. These factories are used by {@link RhythmOverlayInflater} to inflate declarative config into
* respective overlays. If you make a custom spec layer, you should also create a corresponding
* RhythmSpecLayerFactory and register it within {@link RhythmOverlayInflater#registerFactory(String,
* RhythmSpecLayerFactory)} method.
Concrete factories may implement some sort of caching and provide the same * {@link RhythmSpecLayer} instances for equal {@linkplain ArgumentsBundle ArgumentsBundles} if they can be reused, but * it's not mandatory. Furthermore it’s developer’s responsibility to not mutate the layer if the latter is reused in * multiple overlays.
* * @author Paul Danyliuk */ public interface RhythmSpecLayerFactorygetXxx() methods — subclasses should override this
* method if additional processing is required (e.g. lazy variable dereference, expression evaluation etc).
*
* @param key key of the argument whose value to resolve
* @return string representation of the value
*/
protected String resolveArgument(String key) {
return mArguments.get(key);
}
/**
* {@inheritDoc} For simple arguments bundle, will return the raw string as put in the arguments map by the
* inflater. If you need to change how raw value is resolved, override {@link #resolveArgument(String)}
*/
@Override
public String getString(String key) {
return resolveArgument(key);
}
/**
* {@inheritDoc} Will return the raw string as put in the arguments map by the inflater.
*/
@Override
public String getString(String key, @Nullable String defaultValue) {
String rawValue = resolveArgument(key);
return rawValue != null ? rawValue : defaultValue;
}
@Override
public int getInt(String key, int defaultValue) {
String rawValue = resolveArgument(key);
return rawValue != null ? Integer.parseInt(rawValue) : defaultValue;
}
@Override
public float getFloat(String key, float defaultValue) {
String rawValue = resolveArgument(key);
return rawValue != null ? Float.parseFloat(rawValue) : defaultValue;
}
@Override
public boolean getBoolean(String key, boolean defaultValue) {
String rawValue = resolveArgument(key);
if (rawValue != null) {
return Boolean.parseBoolean(rawValue);
} else {
return mArguments.containsKey(key) || defaultValue;
}
}
@Override
@ColorInt
public int getColor(String key, @ColorInt int defaultValue) {
String rawValue = resolveArgument(key);
return rawValue != null ? Color.parseColor(rawValue) : defaultValue;
}
/**
* {@inheritDoc} Does a quick and rough parsing of the raw string for containing constant words like
* top or center_vertical
*/
@Override
@SuppressLint("RtlHardcoded")
public int getGravity(String key, int defaultValue) {
String gravityArg = resolveArgument(key);
if (gravityArg == null) {
return defaultValue;
} else if (gravityArg.equals("center")) {
return Gravity.CENTER;
} else if (gravityArg.equals("fill")) {
return Gravity.FILL;
} else {
// supported options
int gravity = 0;
if (gravityArg.contains("top")) {
gravity |= Gravity.TOP;
}
if (gravityArg.contains("bottom")) {
gravity |= Gravity.BOTTOM;
}
if (gravityArg.contains("center_vertical")) {
gravity |= Gravity.CENTER_VERTICAL;
}
if (gravityArg.contains("fill_vertical")) {
gravity |= Gravity.FILL_VERTICAL;
}
if (gravityArg.contains("left")) {
gravity |= Gravity.LEFT;
}
if (gravityArg.contains("right")) {
gravity |= Gravity.RIGHT;
}
if (gravityArg.contains("center_horizontal")) {
gravity |= Gravity.CENTER_HORIZONTAL;
}
if (gravityArg.contains("fill_horizontal")) {
gravity |= Gravity.FILL_HORIZONTAL;
}
return gravity;
}
}
@Override
@SuppressLint("RtlHardcoded")
@EdgeAffinity
public int getEdgeAffinity(String key, @EdgeAffinity int defaultValue) {
String gravityArg = resolveArgument(key);
if ("top".equals(gravityArg)) {
return Gravity.TOP;
} else if ("left".equals(gravityArg)) {
return Gravity.LEFT;
} else if ("right".equals(gravityArg)) {
return Gravity.RIGHT;
} else if ("bottom".equals(gravityArg)) {
return Gravity.BOTTOM;
} else {
return defaultValue;
}
}
/**
* {@inheritDoc} Note: this is a very crude implementation relying only on the check of trailing string
* characters (i.e. whether the string ends with "dp", "px", "%" etc). However, for development-time library and
* assuming that developers are not their own enemies, that should be fine.
*/
@Override
@DimensionUnits
public int getDimensionUnits(String key) {
String value = resolveArgument(key);
if (value == null) {
return UNITS_NULL;
} else if (value.endsWith("dp") || value.endsWith("dip")) {
return UNITS_DP;
} else if (value.endsWith("px")) {
return UNITS_PX;
} else if (value.endsWith("%")) {
return UNITS_PERCENT;
} else if (value.endsWith("sp")) {
return UNITS_SP;
} else if (value.endsWith("pt")) {
return UNITS_PT;
} else if (value.endsWith("in")) {
return UNITS_IN;
} else if (value.endsWith("mm")) {
return UNITS_MM;
} else {
// assume raw number, try to parse as float
return UNITS_NUMBER;
}
}
/**
* {@inheritDoc}
*
* @see #getDimensionPixelRaw(float, int, DisplayMetrics)
*/
@Override
public float getDimensionValue(String key, float defaultValue) {
String value = resolveArgument(key);
if (value == null) {
return defaultValue;
}
Matcher matcher = DIMEN_VALUE_PATTERN.matcher(value);
if (matcher.find()) {
return Float.parseFloat(matcher.group());
} else {
return defaultValue;
}
}
/**
* {@inheritDoc} Note: this method requires that {@link DisplayMetrics} object is injected in this config.
*
* @see #getDimensionPixelRaw(float, int, DisplayMetrics)
*/
@Override
public float getDimensionPixelExact(String key, float defaultValue) {
@DimensionUnits int units = getDimensionUnits(key);
if (units == UNITS_NULL) {
return defaultValue;
}
float rawValue = getDimensionValue(key, defaultValue);
return getDimensionPixelRaw(rawValue, units, mMetrics);
}
/**
* {@inheritDoc} Note: this method requires that {@link DisplayMetrics} object is injected in this config.
*
* @see #getDimensionPixelRaw(float, int, DisplayMetrics)
*/
@Override
public int getDimensionPixelOffset(String key, int defaultValue) {
@DimensionUnits int units = getDimensionUnits(key);
if (units == UNITS_NULL) {
return defaultValue;
}
float rawValue = getDimensionValue(key, defaultValue);
return (int) getDimensionPixelRaw(rawValue, units, mMetrics);
}
/**
* {@inheritDoc} Note: this method requires that {@link DisplayMetrics} object is injected in this config.
*
* @see #getDimensionPixelRaw(float, int, DisplayMetrics)
*/
@Override
public int getDimensionPixelSize(String key, int defaultValue) {
@DimensionUnits int units = getDimensionUnits(key);
float rawValue = getDimensionValue(key, defaultValue);
float result = getDimensionPixelRaw(rawValue, units, mMetrics);
final int res = (int) (result + 0.5f);
if (res != 0) { return res; }
if (rawValue == 0) { return 0; }
if (rawValue > 0) { return 1; }
return defaultValue;
}
/**
* Convert complex dimension value of provided units into pixels.
*
* @param value raw dimension value, e.g. 24f
* @param units dimension units, one of {@link #UNITS_PX}, {@link #UNITS_DP}, {@link #UNITS_SP}, {@link
* #UNITS_PT}, {@link #UNITS_IN}, {@link #UNITS_MM}, {@link #UNITS_NUMBER}, {@link #UNITS_NULL}, or
* {@link #UNITS_PERCENT}
* @param metrics display metrics to convert complex dimension types that depend on density (dp, sp etc) into
* pixels, can be null if type is one of {@link #UNITS_PX}, {@link #UNITS_PERCENT}, {@link
* #UNITS_NUMBER}, or {@link #UNITS_NULL}
* @return dimension value in pixels
*/
public static float getDimensionPixelRaw(float value, @DimensionUnits int units, DisplayMetrics metrics) {
switch (units) {
case UNITS_DP:
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, metrics);
case UNITS_PX:
case UNITS_PERCENT:
case UNITS_NUMBER:
case UNITS_NULL:
return value;
case UNITS_SP:
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, value, metrics);
case UNITS_PT:
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PT, value, metrics);
case UNITS_IN:
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_IN, value, metrics);
case UNITS_MM:
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, value, metrics);
default:
return 0;
}
}
@Override
public boolean equals(Object o) {
if (this == o) { return true; }
if (o == null || getClass() != o.getClass()) { return false; }
return mArguments.equals(((SimpleArgumentsBundle) o).mArguments);
}
@Override
public int hashCode() {
return mArguments.hashCode();
}
}
================================================
FILE: rhythm/src/main/java/com/actinarium/rhythm/SimpleCacheFactory.java
================================================
/*
* Copyright (C) 2016 Actinarium
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.actinarium.rhythm;
import java.util.HashMap;
import java.util.Map;
/**
* A decorator for a spec layer factory that performs simple caching of previously inflated layers
*
* @author Paul Danyliuk
*/
public class SimpleCacheFactory/res/raw folder without extension
* @return List of lines read from the file
*/
public static ListCreate spec layer that will evenly divide current bounds in given number of columns and then draw all child * layers in each.
This is a minimum constructor for the factory — only paints and reusable objects are * initialized. Developers extending this class are responsible for setting all fields to proper argument * values.
*/ protected Columns() { super(); } /** * Set the number of columns * * @param columnCount number of columns, must be a positive integer * @return this for chaining */ public Columns setColumnCount(@IntRange(from = 1) int columnCount) { mColumnCount = columnCount; return this; } @Override public void draw(Canvas canvas, Rect drawableBounds) { mTemp.set(drawableBounds); final int left = drawableBounds.left; final float width = drawableBounds.width(); for (int i = 1; i <= mColumnCount; i++) { // Always adding rounded i/count fraction of width to the fixed left to ensure symmetry // and that the bounds don't overflow overall width mTemp.right = left + (int) Math.floor(width * i / mColumnCount + 0.5f); // Draw all children into the column super.draw(canvas, mTemp); // Offset the temporary rect mTemp.left = mTemp.right; } } /** * A default factory that creates new {@link Columns} layers from config lines according to the docs */ public static class Factory implements RhythmSpecLayerFactorysp,
* or {@link #DEFAULT_SCALE_FACTOR} (1f) to get pixels.
* @return this for chaining
*/
public DimensionsLabel setScaleFactor(@FloatRange(from = 0.0, fromInclusive = false) float scaleFactor) {
mScaleFactor = scaleFactor;
return this;
}
/**
* Set label gravity. Default is bottom right.
*
* @param gravity Desired gravity. Can be combinations, e.g. {@link Gravity#BOTTOM} | {@link
* Gravity#LEFT}
* @return this for chaining
*/
public DimensionsLabel setGravity(int gravity) {
mGravity = gravity;
return this;
}
/**
* Set label background color
*
* @param color Label background color, in #AARRGGBB format as usual
* @return this for chaining
*/
public DimensionsLabel setBackgroundColor(@ColorInt int color) {
mBackgroundPaint.setColor(color);
return this;
}
/**
* Set the color of the label text itself
*
* @param color Label text color, in #AARRGGBB format as usual
* @return this for chaining
*/
public DimensionsLabel setTextColor(@ColorInt int color) {
mTextPaint.setColor(color);
return this;
}
/**
* Set text size
*
* @param size Text size, in pixels
* @return this for chaining
*/
public DimensionsLabel setTextSize(@FloatRange(from = 0.0, fromInclusive = false) float size) {
mTextPaint.setTextSize(size);
return this;
}
@Override
public void draw(Canvas canvas, Rect drawableBounds) {
final int intWidth = drawableBounds.width();
// Make the label text based on width, height, and scale factor
String text = prettyPrintDips(intWidth, mScaleFactor) + ' ' + MULTIPLY + ' '
+ prettyPrintDips(drawableBounds.height(), mScaleFactor);
// Use StaticLayout, which will calculate text dimensions nicely, then position the box using Gravity.apply()
// (although that's one instantiation per draw call...)
// This is what happens if you're obsessed with perfection like me
StaticLayout layout = new StaticLayout(text, mTextPaint, intWidth, Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false);
Gravity.apply(mGravity, (int) (layout.getLineMax(0) + 0.5), layout.getHeight(), drawableBounds, mTemp);
// Draw background
canvas.drawRect(mTemp, mBackgroundPaint);
// We have to translate the canvas ourselves, since layout can only draw itself at (0, 0)
canvas.save();
canvas.translate(mTemp.left, mTemp.top);
layout.draw(canvas);
canvas.restore();
}
/**
* Sophisticated conversion of pixels to dips with the use of vulgar fractions (to save screen space)
*
* @param px Pixels to convert to dips
* @param scaleFactor Scale factor, should be equal to {@link DisplayMetrics#density} for px to dp conversion
* @return String formatted with vulgar fraction if needed and possible
*/
public static String prettyPrintDips(int px, float scaleFactor) {
String dip;
if (scaleFactor == 1f) {
dip = String.valueOf(px);
} else if (scaleFactor == 2f) {
dip = String.valueOf(px / 2);
if (px % 2 == 1) {
dip += ONE_HALF;
}
} else if (scaleFactor == 3f) {
dip = String.valueOf(px / 3);
if (px % 3 == 1) {
dip += ONE_THIRD;
} else if (px % 3 == 2) {
dip += TWO_THIRDS;
}
} else if (scaleFactor == 4f) {
dip = String.valueOf(px / 4);
if (px % 4 == 1) {
dip += ONE_FOURTH;
} else if (px % 4 == 2) {
dip += ONE_HALF;
} else if (px % 4 == 3) {
dip += THREE_FOURTHS;
}
} else {
// Very hard to determine exactly, so falling back to decimals
dip = DECIMAL_FORMAT.format(px / scaleFactor);
}
return dip;
}
/**
* A default factory that creates new {@link DimensionsLabel} layers from config lines according to the docs
*/
public static class Factory implements RhythmSpecLayerFactoryCreate a spec layer that displays dimensions label.
This is a minimum constructor for the factory * — only paints and reusable objects are initialized. Developers extending this class are responsible for * setting all fields to proper argument values.
*/ protected GridLines() { mPaint = new Paint(); mPaint.setStyle(Paint.Style.FILL); } /** * Set the step of grid lines * * @param step Grid step, in pixels. Allows for float values to properly accommodate devices with non-round * dip-to-pixel ratio (1.5x on hdpi, 2.5x on Nexus 5X etc) * @return this for chaining */ public GridLines setStep(@FloatRange(from = 0f, fromInclusive = false) float step) { mStep = step; return this; } /** * Set edge affinity of the grid * * @param edgeAffinity Controls grid alignment and orientation. Use {@link Gravity#TOP} or {@link * Gravity#BOTTOM} for horizontal lines counting from top or bottom, and {@link Gravity#LEFT} or * {@link Gravity#RIGHT} for vertical lines cointing from left or right edge of the screen * respectively. * @return this for chaining */ public GridLines setEdgeAffinity(@ArgumentsBundle.EdgeAffinity int edgeAffinity) { mEdgeAffinity = edgeAffinity; return this; } /** * Set grid line color * * @param color Grid line color, in #AARRGGBB format as usual * @return this for chaining */ public GridLines setColor(@ColorInt int color) { mPaint.setColor(color); return this; } /** * Set grid line thickness * * @param thickness Grid line thickness, in pixels * @return this for chaining */ public GridLines setThickness(@IntRange(from = 1) int thickness) { mThickness = thickness; return this; } /** * Set the maximum number of steps to outline, respecting layer’s gravity (i.e. if gravity is set to {@link * Gravity#BOTTOM} and the limit is 4, this layer will draw four lines enclosing 4 cells. Default is no limit. * * @param limit Number of lines to draw. Setting zero or less means no limit. * @return this for chaining */ public GridLines setLimit(int limit) { mLimit = limit > 0 ? limit : Integer.MAX_VALUE; return this; } /** * Set additional grid offset. Might be useful if you need to tweak the position of the grid just a few pixels up or * down, or prevent overdraw when combining a few interleaving grids (e.g. to add a 4dp baseline grid to a 8dp * regular grid you only need to draw each second baseline, which is done with a 8dp step and a 4dp offset). * * @param offset Grid offset in pixels. Regardless of gravity, positive offset means right/down, negative means * left/up * @return this for chaining */ public GridLines setOffset(int offset) { mOffset = offset; return this; } @SuppressLint("RtlHardcoded") @Override public void draw(Canvas canvas, Rect drawableBounds) { // Depending on gravity the orientation, the order of drawing, and the starting point are different if (mEdgeAffinity == Gravity.TOP) { final float top = drawableBounds.top + mOffset + 0.5f; for (int i = 0; i <= mLimit; i++) { int y = (int) (top + mStep * i); if (y >= drawableBounds.bottom) { return; } canvas.drawRect(drawableBounds.left, y, drawableBounds.right, y + mThickness, mPaint); } } else if (mEdgeAffinity == Gravity.BOTTOM) { final float bottom = drawableBounds.bottom + mOffset + 0.5f; for (int i = 0; i <= mLimit; i++) { int y = (int) (bottom - mStep * i); if (y < drawableBounds.top) { return; } canvas.drawRect(drawableBounds.left, y, drawableBounds.right, y + mThickness, mPaint); } } else if (mEdgeAffinity == Gravity.LEFT) { final float left = drawableBounds.left + mOffset + 0.5f; for (int i = 0; i <= mLimit; i++) { int x = (int) (left + mStep * i); if (x >= drawableBounds.right) { return; } canvas.drawRect(x, drawableBounds.top, x + mThickness, drawableBounds.bottom, mPaint); } } else if (mEdgeAffinity == Gravity.RIGHT) { final float right = drawableBounds.right + mOffset + 0.5f; for (int i = 0; i <= mLimit; i++) { int x = (int) (right - mStep * i); if (x < drawableBounds.left) { return; } canvas.drawRect(x, drawableBounds.top, x + mThickness, drawableBounds.bottom, mPaint); } } } /** * A default factory that creates new {@link GridLines} layers from config lines according to the docs */ public static class Factory implements RhythmSpecLayerFactorytrue if value is in percent, false if in pixels
* @return this for chaining
*/
public Inset setTop(int value, boolean isPercent) {
mIsTopSet = true;
mTop = value;
mIsTopPercent = isPercent;
return this;
}
/**
* Set bottom inset
*
* @param value pixels or percent
* @param isPercent true if value is in percent, false if in pixels
* @return this for chaining
*/
public Inset setBottom(int value, boolean isPercent) {
mIsBottomSet = true;
mBottom = value;
mIsBottomPercent = isPercent;
return this;
}
/**
* Set left inset
*
* @param value pixels or percent
* @param isPercent true if value is in percent, false if in pixels
* @return this for chaining
*/
public Inset setLeft(int value, boolean isPercent) {
mIsLeftSet = true;
mLeft = value;
mIsLeftPercent = isPercent;
return this;
}
/**
* Set right inset
*
* @param value pixels or percent
* @param isPercent true if value is in percent, false if in pixels
* @return this for chaining
*/
public Inset setRight(int value, boolean isPercent) {
mIsRightSet = true;
mRight = value;
mIsRightPercent = isPercent;
return this;
}
/**
* Set width. If both width, left inset, and right inset are set, right inset is ignored
*
* @param value pixels or percent
* @param isPercent true if value is in percent, false if in pixels
* @return this for chaining
*/
public Inset setWidth(int value, boolean isPercent) {
mIsWidthSet = true;
mWidth = value;
mIsWidthPercent = isPercent;
return this;
}
/**
* Set height. If both height, top inset, and bottom inset are set, bottom inset is ignored
*
* @param value pixels or percent
* @param isPercent true if value is in percent, false if in pixels
* @return this for chaining
*/
public Inset setHeight(int value, boolean isPercent) {
mIsHeightSet = true;
mHeight = value;
mIsHeightPercent = isPercent;
return this;
}
@Override
public void draw(Canvas canvas, Rect drawableBounds) {
// Assume this is a) not called very often, and b) is a fast operation anyway
recalculateInsetRect(drawableBounds);
final int state = canvas.save();
if (mMode != MODE_NO_CLIP) {
canvas.clipRect(mInsetRect);
}
if (mMode == MODE_CLIP_ONLY) {
// Draw sub-layers within original bounds
super.draw(canvas, drawableBounds);
} else {
// Draw sub-layers within new bounds
super.draw(canvas, mInsetRect);
}
canvas.restoreToCount(state);
}
/**
* Update the inset bounds based on provided outer bounds and this layer's state
*
* @param outerBounds Outer bounds provided to this inset layer to modify
*/
protected void recalculateInsetRect(Rect outerBounds) {
final int parentWidth = outerBounds.width();
final int parentHeight = outerBounds.height();
if (!mIsWidthSet) {
// No width - inset based on left and right. Assume those are set, otherwise those are 0 anyway
mInsetRect.left = outerBounds.left + (mIsLeftPercent ? parentWidth * mLeft / 100 : mLeft);
mInsetRect.right = outerBounds.right - (mIsRightPercent ? parentWidth * mRight / 100 : mRight);
} else if (mIsRightSet && !mIsLeftSet) {
// Width and right are set, left not set but calculated from width
mInsetRect.right = outerBounds.right - (mIsRightPercent ? parentWidth * mRight / 100 : mRight);
mInsetRect.left = mInsetRect.right - (mIsWidthPercent ? parentWidth * mWidth / 100 : mWidth);
} else {
// If right not set, or all three are set, right is ignored and calculated as left + width
mInsetRect.left = outerBounds.left + (mIsLeftPercent ? parentWidth * mLeft / 100 : mLeft);
mInsetRect.right = mInsetRect.left + (mIsWidthPercent ? parentWidth * mWidth / 100 : mWidth);
}
if (!mIsHeightSet) {
// No height - inset based on top and bottom. Assume those are set, otherwise those are 0 anyway
mInsetRect.top = outerBounds.top + (mIsTopPercent ? parentHeight * mTop / 100 : mTop);
mInsetRect.bottom = outerBounds.bottom - (mIsBottomPercent ? parentHeight * mBottom / 100 : mBottom);
} else if (mIsBottomSet && !mIsTopSet) {
// Height and bottom are set, top not set but calculated from height
mInsetRect.bottom = outerBounds.bottom - (mIsBottomPercent ? parentHeight * mBottom / 100 : mBottom);
mInsetRect.top = mInsetRect.bottom - (mIsHeightPercent ? parentHeight * mHeight / 100 : mHeight);
} else {
// If bottom not set, or all three are set, bottom is ignored and calculated as top + height
mInsetRect.top = outerBounds.top + (mIsTopPercent ? parentHeight * mTop / 100 : mTop);
mInsetRect.bottom = mInsetRect.top + (mIsHeightPercent ? parentHeight * mHeight / 100 : mHeight);
}
}
/**
* A default factory that creates new {@link Inset} layers from config lines according to the docs
*/
public static class Factory implements RhythmSpecLayerFactoryCreate a layer that draws a horizontal or vertical keyline at a specified distance from required edge.
*This is a minimum constructor for the factory — only paints and reusable objects are initialized. * Developers extending this class are responsible for setting all fields to proper argument values.
*/ protected Keyline() { mPaint = new Paint(); mPaint.setStyle(Paint.Style.FILL); } /** * Set the distance of the keyline from specified edge * * @param distance Distance of this keyline from the specified edge, in pixels * @return this for chaining */ public Keyline setDistance(int distance) { mDistance = distance; return this; } /** * Set edge affinity of the keyline * * @param edgeAffinity Defines the edge of the view this keyline must be anchored to. Values ({@link Gravity#LEFT} * and {@link Gravity#RIGHT}) will result in a vertical keyline, and values ({@link Gravity#TOP} * and {@link Gravity#BOTTOM}) will result in a horizontal keyline. * @return this for chaining */ public Keyline setEdgeAffinity(int edgeAffinity) { mEdgeAffinity = edgeAffinity; return this; } /** * Set keyline color * * @param color Grid line color, in #AARRGGBB format as usual * @return this for chaining */ public Keyline setColor(@ColorInt int color) { mPaint.setColor(color); return this; } /** * Set keyline thickness * * @param thickness Keyline thickness, in pixels. For keylines keep thickness around a few pixels, whereas for * highlights feel free to use as many dips as required. * @return this for chaining * @see #setAlignOutside(boolean) */ public Keyline setThickness(@IntRange(from = 1) int thickness) { mThickness = thickness; return this; } /** * Set keyline alignment. By default, the keyline is drawn towards the specified edge, i.e. if edge affinity is * BOTTOM, distance is 24px and thickness is 6px, the keyline will appear as a horizontal rectangle starting at the * 18th and ending at the 23rd pixel row from the bottom. You can use this method to override that behavior and make * the keyline face outwards (24th to 29th pixel rows in aforementioned example). * * @param alignOutside eitherfalse ({@link #ALIGN_INSIDE}, default) for the keyline to extend towards
* the edge defined by edge affinity, or true ({@link #ALIGN_OUTSIDE}) to extend
* away from the edge
* @return this for chaining
*/
public Keyline setAlignOutside(boolean alignOutside) {
mAlignOutside = alignOutside;
return this;
}
@SuppressLint("RtlHardcoded")
@Override
public void draw(Canvas canvas, Rect drawableBounds) {
if (mEdgeAffinity == Gravity.LEFT) {
// Vertical line at offset points from the left
final int rightX = drawableBounds.left + mDistance + (mAlignOutside ? mThickness : 0);
canvas.drawRect(rightX - mThickness, drawableBounds.top, rightX, drawableBounds.bottom, mPaint);
} else if (mEdgeAffinity == Gravity.RIGHT) {
// Vertical line at offset points from the right
final int leftX = drawableBounds.right - mDistance - (mAlignOutside ? mThickness : 0);
canvas.drawRect(leftX, drawableBounds.top, leftX + mThickness, drawableBounds.bottom, mPaint);
} else if (mEdgeAffinity == Gravity.TOP) {
// Horizontal line at offset points from the top
final int bottomY = drawableBounds.top + mDistance + (mAlignOutside ? mThickness : 0);
canvas.drawRect(drawableBounds.left, bottomY - mThickness, drawableBounds.right, bottomY, mPaint);
} else if (mEdgeAffinity == Gravity.BOTTOM) {
// Horizontal line at offset points from the top
final int topY = drawableBounds.bottom - mDistance - (mAlignOutside ? mThickness : 0);
canvas.drawRect(drawableBounds.left, topY, drawableBounds.right, topY + mThickness, mPaint);
}
}
/**
* A default factory that creates new {@link Keyline} layers from config lines according to the docs
*/
public static class Factory implements RhythmSpecLayerFactoryA controller that interconnects {@link RhythmGroup}s, {@link RhythmFrameLayout}s, and the Quick Control * notification, and should be used as an entry point to accessing Rhythm library programmatically. For proper function * a singleton Rhythm control must be accessible from ApplicationContext (i.e. the app’s {@link Application} object must * implement {@link Host}).
Note: if you don’t need the notification or RhythmicFrameLayouts,
* you might actually not need a Rhythm control in your project — just instantiate and use {@link RhythmGroup}s
* directly.
null if you don’t need the Quick Control
* notification.
*/
public RhythmControl(@Nullable Context context) {
mContext = context;
mRhythmGroups = new ArrayList<>();
}
/**
* Make a new Rhythm group, registered in this Rhythm control
*
* @param title A convenient title for this group to identify it in the notification. Not mandatory (can be
* null) but recommended.
* @return The created Rhythm group instance, managed by this control
*/
public RhythmGroup makeGroup(String title) {
final RhythmGroup group = new RhythmGroup();
group.mTitle = title;
group.mIndex = mRhythmGroups.size();
group.mControl = this;
mRhythmGroups.add(group);
// If this was the first group, and the notification is already shown, set it to display the first group
if (mCurrentNotificationGroupIndex == NOTIFICATION_NO_GROUPS) {
mCurrentNotificationGroupIndex = 0;
requestNotificationUpdate();
}
return group;
}
/**
* Get Rhythm group at requested index
*
* @param index index of the group (0, 1, 2... in order of adding)
* @return requested Rhythm group
*/
public RhythmGroup getGroup(int index) {
return mRhythmGroups.get(index);
}
/**
* @return the number of groups registered in this control
*/
public int getGroupCount() {
return mRhythmGroups.size();
}
/**
* Show the “Quick Control” notification, which allows to switch overlays for all registered Rhythm * groups quickly without navigating away from your app. Usually you would want to call this once during initial * configuration (unless you don’t need the notification).
Note: Quick Control notification is * dismissible. Upon dismiss, all Rhythm overlays will be hidden. There’s no way to bring it back other than kill * and restart the application, unless you explicitly create a button, a menu option etc in your application that * would conjure the notification again by calling this method.
* * @param notificationId ID for Rhythm notification, must be unique across the app */ public void showQuickControl(int notificationId) { if (mContext == null) { return; } // Remember the notification ID for reuse on update mNotificationId = notificationId; // If notification isn't displayed already, display the first group (or no groups notice) if (mCurrentNotificationGroupIndex == NOTIFICATION_OFF) { mCurrentNotificationGroupIndex = mRhythmGroups.isEmpty() ? NOTIFICATION_NO_GROUPS : 0; } requestNotificationUpdate(); } /** * Sets all registered drawables in all managed groups to display no Rhythm overlays; sets notification state to * hidden */ void onNotificationDismiss() { mCurrentNotificationGroupIndex = NOTIFICATION_OFF; for (int i = 0, size = mRhythmGroups.size(); i < size; i++) { mRhythmGroups.get(i).selectOverlay(RhythmGroup.NO_OVERLAY); } } /** * Should be called whenever notification state is changed (e.g. when cycling through the groups or overlays) */ void requestNotificationUpdate() { if (mCurrentNotificationGroupIndex != NOTIFICATION_OFF) { RhythmNotificationService.showNotification(mContext, mNotificationId); } } RhythmGroup getCurrentNotificationGroup() { return mCurrentNotificationGroupIndex < 0 ? null : mRhythmGroups.get(mCurrentNotificationGroupIndex); } void selectNextNotificationGroup() { // Assume that this method can be called only when valid notification is displayed for a group with index >= 0 // Increment by 1 and wrap if that was the last one. And request notification update mCurrentNotificationGroupIndex = ++mCurrentNotificationGroupIndex % mRhythmGroups.size(); requestNotificationUpdate(); } /** * The {@link Application} must implement this interface to provide the singleton {@link RhythmControl} instance * through its method {@link #getRhythmControl()} to {@link RhythmFrameLayout}s and the Quick Control notification */ public interface Host { /** * Get the {@link RhythmControl} of this application to access any {@link RhythmGroup} and the rest of Rhythm * API * * @return Rhythm control associated with this application */ RhythmControl getRhythmControl(); } } ================================================ FILE: rhythm-control/src/main/java/com/actinarium/rhythm/control/RhythmFrameLayout.java ================================================ /* * Copyright (C) 2016 Actinarium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.actinarium.rhythm.control; import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Build; import android.support.annotation.IntDef; import android.support.annotation.Nullable; import android.util.AttributeSet; import android.widget.FrameLayout; import com.actinarium.rhythm.RhythmDrawable; import com.actinarium.rhythm.RhythmOverlay; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * A {@link FrameLayout} implementation with rich Rhythm support. You can use this layout to wrap existing views and * draw a Rhythm overlay from specified group. The overlay can be positioned either under the view, over the view, or * just under/over the content (see {@link #setOverlayPosition(int)}). Both the group and overlay position can be set in * the layout XML with attributesapp:rhythmGroup and app:overlayPosition respectively.
*
* @author Paul Danyliuk
* @version $Id$
*/
public class RhythmFrameLayout extends FrameLayout {
/**
* Use this value to indicate that this view is not connected to any {@link RhythmGroup} and shouldn’t display any
* overlay
*/
public static final int NO_GROUP = -1;
// The following are to control where the overlay will be drawn
// todo: test how overlay position affects its properties on view's translation, rotation, scaling etc
/**
* Draw the overlay under the background of this view. Pretty useless if this view has opaque background.
*/
public static final int OVERLAY_POSITION_UNDER_BACKGROUND = 0;
/**
* Draw the overlay over view’s background but under child views. Default choice: useful yet non-obtrusive.
*/
public static final int OVERLAY_POSITION_UNDER_CONTENT = 1;
/**
* Draw the overlay over the view’s content (sans foreground). Use this mode if you have nested opaque views that
* occlude the overlay, and there are elements within, which you still need to align.
*/
public static final int OVERLAY_POSITION_OVER_CONTENT = 2;
/**
* Same as {@link #OVERLAY_POSITION_OVER_CONTENT}, but this also draws over any foreground (ripples, touch
* highlights etc).
*/
public static final int OVERLAY_POSITION_OVER_FOREGROUND = 3;
/**
* Index of the group this view should get its {@link RhythmDrawable} from, or {@link #NO_GROUP}.
*/
protected int mRhythmGroupIndex;
@OverlayPosition
protected int mOverlayPosition;
/**
* Obtained from {@link RhythmGroup}, which then controls this drawable, telling it what {@link RhythmOverlay} to
* draw. Or null.
*/
protected RhythmDrawable mRhythmDrawable;
/**
* Overlay bounds, relative to this view (since canvas is already translated to the origin point of this view)
*/
protected Rect mBounds = new Rect();
// Constructors
public RhythmFrameLayout(Context context) {
super(context);
mRhythmGroupIndex = NO_GROUP;
mOverlayPosition = OVERLAY_POSITION_UNDER_CONTENT;
setWillNotDraw(false);
}
public RhythmFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
initFromAttrs(context, attrs, 0, 0);
}
public RhythmFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initFromAttrs(context, attrs, defStyleAttr, 0);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public RhythmFrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initFromAttrs(context, attrs, defStyleAttr, defStyleRes);
}
private void initFromAttrs(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
TypedArray array = context
.getTheme()
.obtainStyledAttributes(attrs, R.styleable.RhythmFrameLayout, defStyleAttr, defStyleRes);
try {
int position = array.getInteger(R.styleable.RhythmFrameLayout_overlayPosition, OVERLAY_POSITION_UNDER_CONTENT);
if (position == OVERLAY_POSITION_UNDER_BACKGROUND
|| position == OVERLAY_POSITION_UNDER_CONTENT
|| position == OVERLAY_POSITION_OVER_CONTENT
|| position == OVERLAY_POSITION_OVER_FOREGROUND) {
//noinspection ResourceType
mOverlayPosition = position;
// We need to ensure draw()/onDraw() is called when overlay position is other than over content (otherwise it's drawn elsewhere)
setWillNotDraw(position == OVERLAY_POSITION_OVER_CONTENT);
} else {
mOverlayPosition = OVERLAY_POSITION_UNDER_CONTENT;
setWillNotDraw(false);
}
mRhythmGroupIndex = array.getInteger(R.styleable.RhythmFrameLayout_rhythmGroup, NO_GROUP);
} finally {
array.recycle();
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (changed) {
mBounds.set(0, 0, right - left, bottom - top);
}
if (mRhythmDrawable != null && changed) {
// If there's a drawable, and layout has changed, we need to update its bounds
mRhythmDrawable.setBounds(mBounds);
} else if (mRhythmDrawable == null && mRhythmGroupIndex != NO_GROUP) {
// If the group is set but there's no drawable yet, try to pull it from the group
onRhythmGroupSet();
}
}
@Override
public void draw(Canvas canvas) {
// Draw before or after everything? (if there's anything to draw)
if (mRhythmDrawable == null) {
super.draw(canvas);
} else if (mOverlayPosition == OVERLAY_POSITION_UNDER_BACKGROUND) {
mRhythmDrawable.draw(canvas);
super.draw(canvas);
} else if (mOverlayPosition == OVERLAY_POSITION_OVER_FOREGROUND) {
super.draw(canvas);
mRhythmDrawable.draw(canvas);
} else {
super.draw(canvas);
}
}
@Override
protected void onDraw(Canvas canvas) {
// Draw before content? (if there's anything to draw)
if (mRhythmDrawable != null && mOverlayPosition == OVERLAY_POSITION_UNDER_CONTENT) {
mRhythmDrawable.draw(canvas);
}
super.onDraw(canvas);
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
// Draw over content and children? (if there's anything to draw)
if (mRhythmDrawable != null && mOverlayPosition == OVERLAY_POSITION_OVER_CONTENT) {
mRhythmDrawable.draw(canvas);
}
}
@Override
protected boolean verifyDrawable(Drawable who) {
return (mRhythmDrawable != null && mRhythmDrawable == who) || super.verifyDrawable(who);
}
// Getters/setters
/**
* @return Current Rhythm drawable
*/
public RhythmDrawable getRhythmDrawable() {
return mRhythmDrawable;
}
/**
* Set a different {@link RhythmDrawable} to this view.
*
* @param drawable Rhythm drawable to set, or null to unlink this view from Rhythm
*/
public void setRhythmDrawable(@Nullable RhythmDrawable drawable) {
doSetRhythmDrawable(drawable);
invalidate();
}
/**
* @return Index of the Rhythm group this view is linked to, or {@link #NO_GROUP}
*/
public int getRhythmGroupIndex() {
return mRhythmGroupIndex;
}
/**
* Link this drawable to a different {@link RhythmGroup} (identified by index), or to no group at all.
*
* @param rhythmGroupIndex Index of required {@link RhythmGroup} in {@link RhythmControl}, or {@link #NO_GROUP}
*/
public void setRhythmGroupIndex(int rhythmGroupIndex) {
mRhythmGroupIndex = rhythmGroupIndex;
if (mRhythmGroupIndex != NO_GROUP) {
onRhythmGroupSet();
} else {
doSetRhythmDrawable(null);
}
invalidate();
}
/**
* @return Overlay position
* @see #OVERLAY_POSITION_UNDER_BACKGROUND
* @see #OVERLAY_POSITION_UNDER_CONTENT
* @see #OVERLAY_POSITION_OVER_CONTENT
* @see #OVERLAY_POSITION_OVER_FOREGROUND
*/
public int getOverlayPosition() {
return mOverlayPosition;
}
/**
* Set overlay position
*
* @param overlayPosition New overlay position, one of overlay position constants
* @see #OVERLAY_POSITION_UNDER_BACKGROUND
* @see #OVERLAY_POSITION_UNDER_CONTENT
* @see #OVERLAY_POSITION_OVER_CONTENT
* @see #OVERLAY_POSITION_OVER_FOREGROUND
*/
public void setOverlayPosition(@OverlayPosition int overlayPosition) {
if (mOverlayPosition != overlayPosition) {
mOverlayPosition = overlayPosition;
setWillNotDraw(overlayPosition == OVERLAY_POSITION_OVER_CONTENT);
invalidate();
}
}
/**
* Retrieves proper drawable from current group and links it to this view
*/
private void onRhythmGroupSet() {
// Request rhythm group from application context
Context context = getContext().getApplicationContext();
if (context instanceof RhythmControl.Host) {
// This may fail with index out of bounds exception if incorrect group index is provided
// But, IMHO, it is better to throw the exception than suppress it and leave the developer clueless
final RhythmDrawable drawable = ((RhythmControl.Host) context).getRhythmControl()
.getGroup(mRhythmGroupIndex)
.makeDrawable();
doSetRhythmDrawable(drawable);
} else {
// Uh-oh
throw new ClassCastException(this + " cannot connect to RhythmControl. " +
"Check if your Application implements RhythmControl.Host");
}
}
/**
* Links new drawable to this view
*/
private void doSetRhythmDrawable(@Nullable RhythmDrawable drawable) {
if (mRhythmDrawable != null) {
mRhythmDrawable.setCallback(null);
}
mRhythmDrawable = drawable;
if (mRhythmDrawable != null) {
mRhythmDrawable.setBounds(mBounds);
mRhythmDrawable.setCallback(this);
}
}
/**
* Type def annotation for overlay position enum
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({OVERLAY_POSITION_UNDER_BACKGROUND, OVERLAY_POSITION_UNDER_CONTENT, OVERLAY_POSITION_OVER_CONTENT, OVERLAY_POSITION_OVER_FOREGROUND})
public @interface OverlayPosition {
}
}
================================================
FILE: rhythm-control/src/main/java/com/actinarium/rhythm/control/RhythmGroup.java
================================================
/*
* Copyright (C) 2016 Actinarium
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.actinarium.rhythm.control;
import android.graphics.drawable.Drawable;
import android.view.View;
import android.widget.FrameLayout;
import com.actinarium.rhythm.RhythmDrawable;
import com.actinarium.rhythm.RhythmOverlay;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
/**
* Controls a group of {@link RhythmDrawable}s, namely propagates the same {@link RhythmOverlay} for all registered
* RhythmDrawables to render. A {@link RhythmGroup} object holds onto a list of RhythmOverlays
* and allows cycling through them. Usually, Rhythm groups are used within a {@link RhythmControl} and instantiated via
* {@link RhythmControl#makeGroup(String)}) method, however you are free to make “orphaned” groups and
* control them explicitly from your app.
Also contains convenience methods for easy decoration of existing views * — those can simplify the scenario when you don’t want to include Rhythm into production builds.
* * @author Paul Danyliuk */ public final class RhythmGroup { public static int NO_OVERLAY = -1; private static final int ESTIMATED_OVERLAYS_PER_GROUP = 4; String mTitle; // Assigned by RhythmControl upon instantiation via {@link RhythmControl#makeGroup(String)}; makes no sense otherwise int mIndex; RhythmControl mControl; private ListCreate a new Rhythm group.
Heads up: do not explicitly call new RhythmGroup() unless
* you specifically don’t want it attached to a {@link RhythmControl} (i.e. don’t want it to appear in the Quick
* Control notification). Instead, you should use {@link RhythmControl#makeGroup(String)}.
null if the group is anonymous
*/
public String getTitle() {
return mTitle;
}
/**
* Add Rhythm overlay to this group
*
* @param overlay The Rhythm overlay to add
* @return this for chaining
*/
public RhythmGroup addOverlay(RhythmOverlay overlay) {
mOverlays.add(overlay);
if (mCurrentOverlayIndex == NO_OVERLAY) {
selectOverlay(0);
}
return this;
}
/**
* Add multiple Rhythm overlays to this group
*
* @param overlays The Rhythm overlays to add
* @return this for chaining
*/
public RhythmGroup addOverlays(CollectionA handy method that will decorate provided views with {@link RhythmDrawable}s controlled by this group.
*Note: the backgrounds of all provided views will be wrapped and replaced by
* RhythmDrawables. To obtain the original background drawables you have to call {@link
* RhythmDrawable#getDecorated() view.getBackground().getDecorated()}.
decorate(View...), wraps and replaces existing foreground drawable with {@link RhythmDrawable}.
*
* @param views Frame layouts whose foregrounds should be decorated
* @see #decorate(View...)
*/
public void decorateForeground(FrameLayout... views) {
for (FrameLayout view : views) {
RhythmDrawable decoratingRhythmDrawable = makeDrawable();
decoratingRhythmDrawable.setDecorated(view.getForeground());
view.setForeground(decoratingRhythmDrawable);
}
}
/**
* @return Index of currently selected overlay, or {@link #NO_OVERLAY}
* @see #getCurrentOverlay()
* @see #getOverlayCount()
*/
public int getCurrentOverlayIndex() {
return mCurrentOverlayIndex;
}
/**
* @return Number of overlays associated with this group
* @see #getCurrentOverlayIndex()
*/
public int getOverlayCount() {
return mOverlays.size();
}
/**
* @return Currently selected overlay, or null if overlay is disabled
* @see #getCurrentOverlayIndex()
*/
public RhythmOverlay getCurrentOverlay() {
return mCurrentOverlayIndex != NO_OVERLAY ? mOverlays.get(mCurrentOverlayIndex) : null;
}
/**
* Select overlay by index. Provide {@link #NO_OVERLAY} to hide overlay.
*
* @param index Overlay index, or {@link #NO_OVERLAY}
* @see #selectNextOverlay()
* @see #getOverlayCount()
*/
public void selectOverlay(int index) {
if (index == NO_OVERLAY || (index >= 0 && index < mOverlays.size())) {
if (mCurrentOverlayIndex != index) {
mCurrentOverlayIndex = index;
doSetOverlay(getCurrentOverlay());
}
} else {
throw new IndexOutOfBoundsException("The index is neither NO_OVERLAY nor valid.");
}
}
/**
* Convenience method to cycle through overlays. Meant primarily for use in Quick Control notification, but can be
* invoked programmatically.
*
* @see #selectOverlay(int)
*/
public void selectNextOverlay() {
if (mCurrentOverlayIndex == NO_OVERLAY) {
if (mOverlays.isEmpty()) {
// Still no overlay, so no-op.
return;
}
mCurrentOverlayIndex = 0;
} else {
mCurrentOverlayIndex = ++mCurrentOverlayIndex % mOverlays.size();
// Disabling overlay after the last one
if (mCurrentOverlayIndex == 0) {
mCurrentOverlayIndex = NO_OVERLAY;
}
}
doSetOverlay(getCurrentOverlay());
}
@Override
public String toString() {
return mTitle != null ? mTitle : "Group #" + mIndex;
}
/**
* Propagates current overlay to all linked {@link RhythmDrawable}s, removing dead references on the way. Also
* updates the notification to reflect current overlay’s name
*
* @todo add possibility to propagate arbitrary overlay, not just one of those in the list
*/
private void doSetOverlay(RhythmOverlay overlay) {
// Using iterator here because we need to remove elements halfway
Iterator