diff --git a/app/components/layertimeline/layertimeline.html b/app/components/layertimeline/layertimeline.html
index 74371d8..760bd4e 100644
--- a/app/components/layertimeline/layertimeline.html
+++ b/app/components/layertimeline/layertimeline.html
@@ -70,6 +70,26 @@
Animated vector drawable(s)
+
+
+ Animated vector drawable(s) with Attrs & Colors & Styles
+
+
+
+
+ Attrs xml
+
+
+
+
+ Colors xml
+
+
+
+
+ Styles xml
+
+
diff --git a/app/components/layertimeline/layertimeline.js b/app/components/layertimeline/layertimeline.js
index 2941380..93b31ab 100644
--- a/app/components/layertimeline/layertimeline.js
+++ b/app/components/layertimeline/layertimeline.js
@@ -560,7 +560,39 @@ class LayerTimelineController {
*/
onExportAVDs() {
ga('send', 'event', 'export', 'exportVectorAnimated');
- this.studioState_.exportAVDs();
+ this.studioState_.exportAVDs(false);
+ }
+
+ /**
+ * Handles export to animated vector drawable format with attrs for colors.
+ */
+ onExportAVDsAttrs() {
+ ga('send', 'event', 'export', 'exportVectorAnimatedAttrs');
+ this.studioState_.exportAVDs(true);
+ }
+
+ /**
+ * Handles export to xml with colors.
+ */
+ onExportColorsXml() {
+ ga('send', 'event', 'export', 'exportColorsXml');
+ this.studioState_.exportColorsXml();
+ }
+
+ /**
+ * Handles export to xml with attrs.
+ */
+ onExportAttrsXml() {
+ ga('send', 'event', 'export', 'exportAttrsXml');
+ this.studioState_.exportAttrsXml();
+ }
+
+ /**
+ * Handles export to xml with attrs.
+ */
+ onExportStylesXml() {
+ ga('send', 'event', 'export', 'exportAttrsXml');
+ this.studioState_.exportStylesXml();
}
/**
diff --git a/app/pages/studio/studiostate.js b/app/pages/studio/studiostate.js
index 05acc1f..b7ab772 100644
--- a/app/pages/studio/studiostate.js
+++ b/app/pages/studio/studiostate.js
@@ -14,12 +14,25 @@
* limitations under the License.
*/
-import {default as zip} from 'zipjs-browserify';
-
-import {Artwork, Animation, AnimationBlock, BaseLayer} from 'model';
-import {AnimationRenderer} from 'AnimationRenderer';
-import {AvdSerializer} from 'AvdSerializer';
-import {ModelUtil} from 'ModelUtil';
+import {
+ default as zip
+} from 'zipjs-browserify';
+
+import {
+ Artwork,
+ Animation,
+ AnimationBlock,
+ BaseLayer
+} from 'model';
+import {
+ AnimationRenderer
+} from 'AnimationRenderer';
+import {
+ AvdSerializer
+} from 'AvdSerializer';
+import {
+ ModelUtil
+} from 'ModelUtil';
const CHANGES_TAG = '$$studioState::CHANGES';
@@ -32,8 +45,7 @@ const BLANK_ARTWORK = {
id: new Artwork().typeIdPrefix,
width: 24,
height: 24,
- layers: [
- ]
+ layers: []
};
@@ -93,7 +105,9 @@ class StudioStateService {
set playing(playing) {
this.playing_ = playing;
- this.broadcastChanges_({playing: true});
+ this.broadcastChanges_({
+ playing: true
+ });
}
get artwork() {
@@ -106,7 +120,9 @@ class StudioStateService {
set artwork(artwork) {
this.artwork_ = artwork;
- this.artworkChanged({noUndo:true});
+ this.artworkChanged({
+ noUndo: true
+ });
}
get animations() {
@@ -128,18 +144,26 @@ class StudioStateService {
animChanged(options = {}) {
this.dirty_ = true;
this.rebuildRenderer_();
- this.broadcastChanges_({animations: true});
+ this.broadcastChanges_({
+ animations: true
+ });
if (!options.noUndo) {
- this.saveUndoState_({debounce:true});
+ this.saveUndoState_({
+ debounce: true
+ });
}
}
artworkChanged(options = {}) {
this.dirty_ = true;
this.rebuildRenderer_();
- this.broadcastChanges_({artwork: true});
+ this.broadcastChanges_({
+ artwork: true
+ });
if (!options.noUndo) {
- this.saveUndoState_({debounce:true});
+ this.saveUndoState_({
+ debounce: true
+ });
}
}
@@ -166,7 +190,7 @@ class StudioStateService {
// (commit the current state after N millisec of inactivity)
if (options.debounce) {
this.debouncedSaveUndoPromise_ = this.timeout_(
- () => this.commitUndoStateToTopSlot_(), UNDO_DEBOUNCE_MS);
+ () => this.commitUndoStateToTopSlot_(), UNDO_DEBOUNCE_MS);
} else {
this.commitUndoStateToTopSlot_();
}
@@ -185,12 +209,16 @@ class StudioStateService {
let state = this.undoStates_[this.currentUndoState_];
this.artwork_ = new Artwork(state.artwork);
this.animations_ = state.animations.map(anim => new Animation(anim));
- this.activeAnimation_ = (this.animations_.length > 0 && state.activeAnimationIndex >= 0)
- ? this.animations_[state.activeAnimationIndex]
- : null;
+ this.activeAnimation_ = (this.animations_.length > 0 && state.activeAnimationIndex >= 0) ?
+ this.animations_[state.activeAnimationIndex] :
+ null;
this.selection = [];
- this.artworkChanged({noUndo:true});
- this.animChanged({noUndo:true});
+ this.artworkChanged({
+ noUndo: true
+ });
+ this.animChanged({
+ noUndo: true
+ });
}
tryUndo() {
@@ -241,15 +269,17 @@ class StudioStateService {
this.activeAnimation_ = activeAnimation;
this.rebuildRenderer_();
- this.broadcastChanges_({activeAnimation: true});
+ this.broadcastChanges_({
+ activeAnimation: true
+ });
}
rebuildRenderer_() {
this.animationRenderer_ = null;
if (this.activeAnimation) {
this.animationRenderer_ = new AnimationRenderer(
- this.artwork,
- this.activeAnimation);
+ this.artwork,
+ this.activeAnimation);
this.animationRenderer_.setAnimationTime(this.activeTime_);
}
}
@@ -263,12 +293,14 @@ class StudioStateService {
if (this.animationRenderer_) {
this.animationRenderer_.setAnimationTime(activeTime);
}
- this.broadcastChanges_({activeTime: true});
+ this.broadcastChanges_({
+ activeTime: true
+ });
}
getSelectionByType_(type) {
- return (this.selection_ && this.selection_.length && this.selection_[0] instanceof type)
- ? this.selection_ : [];
+ return (this.selection_ && this.selection_.length && this.selection_[0] instanceof type) ?
+ this.selection_ : [];
}
get selectedLayers() {
@@ -300,13 +332,15 @@ class StudioStateService {
this.selection_.forEach(item => delete item.selected_);
this.selection_ = selection ? selection.slice() : [];
this.selection_.forEach(item => item.selected_ = true);
- this.broadcastChanges_({selection: true});
+ this.broadcastChanges_({
+ selection: true
+ });
}
areItemsMultiselectCompatible_(item1, item2) {
- return !!(!item1 || !item2
- || item1.constructor === item2.constructor
- || item1 instanceof BaseLayer && item2 instanceof BaseLayer);
+ return !!(!item1 || !item2 ||
+ item1.constructor === item2.constructor ||
+ item1 instanceof BaseLayer && item2 instanceof BaseLayer);
}
selectItem(item) {
@@ -351,7 +385,9 @@ class StudioStateService {
}
}
- this.broadcastChanges_({selection: true});
+ this.broadcastChanges_({
+ selection: true
+ });
}
deleteLayers(layersToDelete) {
@@ -423,7 +459,7 @@ class StudioStateService {
}
onChange(fn, $scope) {
- let watcher = this.rootScope_.$on(CHANGES_TAG, function() {
+ let watcher = this.rootScope_.$on(CHANGES_TAG, function () {
// window.setTimeout(() => $scope.$apply(() => fn.apply(this, arguments)), 0);
fn.apply(this, arguments);
});
@@ -435,7 +471,9 @@ class StudioStateService {
let anchor = $('').hide().appendTo(document.body);
let blob = content;
if (!(content instanceof Blob)) {
- blob = new Blob([content], {type: 'octet/stream'});
+ blob = new Blob([content], {
+ type: 'octet/stream'
+ });
}
let url = window.URL.createObjectURL(blob);
anchor.attr({
@@ -494,17 +532,61 @@ class StudioStateService {
this.downloadFile_(xmlStr, `${this.artwork.id}.xml`);
}
- exportAVDs() {
+ exportAttrsXml() {
+ let xmlStr = AvdSerializer.colorToAttrsXmlString(this.artwork);
+ this.downloadFile_(xmlStr, `attrs.xml`);
+ }
+
+ exportColorsXml() {
+ let xmlStr = AvdSerializer.colorToColorsXmlString(this.artwork);
+ this.downloadFile_(xmlStr, `colors.xml`);
+ }
+
+ exportStylesXml() {
+ let xmlStr = AvdSerializer.colorToStylesXmlString(this.artwork);
+ this.downloadFile_(xmlStr, `styles.xml`);
+ }
+
+ exportAVDs(withColorsAttrs) {
if (this.animations.length) {
let exportedAnimations = this.animations.map(animation => ({
animation,
filename: `avd_${this.artwork.id}_${animation.id}.xml`,
- xmlStr: AvdSerializer.artworkAnimationToAvdXmlString(this.artwork, animation)
+ xmlStr: AvdSerializer.artworkAnimationToAvdXmlString(this.artwork, animation, withColorsAttrs)
}));
+
if (exportedAnimations.length == 1) {
- // download a single XML
- this.downloadFile_(exportedAnimations[0].xmlStr, exportedAnimations[0].filename);
+ if (withColorsAttrs) {
+ // download a ZIP
+ zip.createWriter(new zip.BlobWriter(), writer => {
+ // add next file
+ writer.add(
+ exportedAnimations[0].filename,
+ new zip.TextReader(exportedAnimations[0].xmlStr),
+ () => {
+ writer.add(
+ 'attrs.xml',
+ new zip.TextReader(AvdSerializer.colorToAttrsXmlString(this.artwork)),
+ () => {
+ writer.add(
+ 'colors.xml',
+ new zip.TextReader(AvdSerializer.colorToColorsXmlString(this.artwork)),
+ () => {
+ writer.add(
+ 'styles.xml',
+ new zip.TextReader(AvdSerializer.colorToStylesXmlString(this.artwork)),
+ () => {
+ writer.close(blob => this.downloadFile_(blob, `avd_${this.artwork.id}.zip`));
+ });
+ });
+ });
+ });
+ }, error => console.error(error));
+ } else {
+ // download a single XML
+ this.downloadFile_(exportedAnimations[0].xmlStr, exportedAnimations[0].filename);
+ }
} else {
// download a ZIP
zip.createWriter(new zip.BlobWriter(), writer => {
@@ -512,15 +594,35 @@ class StudioStateService {
let next_ = () => {
++i;
if (i >= exportedAnimations.length) {
- // close
- writer.close(blob => this.downloadFile_(blob, `avd_${this.artwork.id}.zip`));
+ if (withColorsAttrs) {
+ writer.add(
+ 'attrs.xml',
+ new zip.TextReader(AvdSerializer.colorToAttrsXmlString(this.artwork)),
+ () => {
+ writer.add(
+ 'colors.xml',
+ new zip.TextReader(AvdSerializer.colorToColorsXmlString(this.artwork)),
+ () => {
+ writer.add(
+ 'styles.xml',
+ new zip.TextReader(AvdSerializer.colorToStylesXmlString(this.artwork)),
+ () => {
+ // close
+ writer.close(blob => this.downloadFile_(blob, `avd_${this.artwork.id}.zip`));
+ });
+ });
+ });
+ } else {
+ // close
+ writer.close(blob => this.downloadFile_(blob, `avd_${this.artwork.id}.zip`));
+ }
} else {
// add next file
let exportedAnimation = exportedAnimations[i];
writer.add(
- exportedAnimation.filename,
- new zip.TextReader(exportedAnimation.xmlStr),
- next_);
+ exportedAnimation.filename,
+ new zip.TextReader(exportedAnimation.xmlStr),
+ next_);
}
};
next_();
@@ -531,4 +633,4 @@ class StudioStateService {
}
-angular.module('AVDStudio').service('StudioStateService', StudioStateService);
+angular.module('AVDStudio').service('StudioStateService', StudioStateService);
\ No newline at end of file
diff --git a/app/scripts/AvdSerializer.js b/app/scripts/AvdSerializer.js
index 4abb15b..f2c1666 100644
--- a/app/scripts/AvdSerializer.js
+++ b/app/scripts/AvdSerializer.js
@@ -16,7 +16,13 @@
import xmlserializer from 'xmlserializer';
-import {Artwork, PathLayer, LayerGroup, MaskLayer, DefaultValues} from './model';
+import {
+ Artwork,
+ PathLayer,
+ LayerGroup,
+ MaskLayer,
+ DefaultValues
+} from './model';
const XMLNS_NS = 'http://www.w3.org/2000/xmlns/';
const ANDROID_NS = 'http://schemas.android.com/apk/res/android';
@@ -24,20 +30,30 @@ const AAPT_NS = 'http://schemas.android.com/aapt';
let conditionalAttr_ = (node, attr, value, skipValue) => {
- if (value !== undefined
- && value !== null
- && (skipValue === undefined || value !== skipValue)) {
+ if (value !== undefined &&
+ value !== null &&
+ (skipValue === undefined || value !== skipValue)) {
node.setAttributeNS(ANDROID_NS, attr, value);
}
};
let serializeXmlNode_ = xmlNode => {
- let xmlStr = xmlserializer.serializeToString(xmlNode, {indent:4, multiAttributeIndent:4});
+ let xmlStr = xmlserializer.serializeToString(xmlNode, {
+ indent: 4,
+ multiAttributeIndent: 4
+ });
return xmlStr; //new XMLSerializer().serializeToString(xmlNode);
// return vkbeautify.xml(xmlStr, 4);
};
+let serializeXmlWithoutIndentNode_ = xmlNode => {
+ let xmlStr = xmlserializer.serializeToString(xmlNode, {
+ indent: 0,
+ multiAttributeIndent: 0
+ });
+ return xmlStr;
+};
export const AvdSerializer = {
@@ -47,16 +63,136 @@ export const AvdSerializer = {
artworkToVectorDrawableXmlString(artwork) {
let xmlDoc = document.implementation.createDocument(null, 'vector');
let rootNode = xmlDoc.documentElement;
- AvdSerializer.artworkToXmlNode_(artwork, rootNode, xmlDoc);
+ AvdSerializer.artworkToXmlNode_(artwork, rootNode, xmlDoc, false);
return serializeXmlNode_(rootNode);
},
+ /**
+ * Serializes an Colors to a colors XML file.
+ */
+ colorToColorsXmlString(artwork) {
+ let xmlColorsDoc = document.implementation.createDocument(null, 'resources');
+ let rootNodeColors = xmlColorsDoc.documentElement;
+ rootNodeColors.innerHTML += '\n';
+
+ artwork.walk((layer, parentNode) => {
+ if (layer instanceof PathLayer) {
+
+ if (layer.fillColor != null && layer.fillColor != '') {
+ let colorNode = xmlColorsDoc.createElement('color');
+ colorNode.setAttribute('name', layer.id + '_color');
+ colorNode.append(layer.fillColor);
+ rootNodeColors.appendChild(colorNode);
+ rootNodeColors.innerHTML += '\n';
+ }
+
+ if (layer.strokeColor != null && layer.strokeColor != '') {
+ let strokeColorNode = xmlColorsDoc.createElement('color');
+ strokeColorNode.setAttribute('name', layer.id + '_stroke_color');
+ strokeColorNode.append(layer.strokeColor);
+ rootNodeColors.appendChild(strokeColorNode);
+ rootNodeColors.innerHTML += '\n';
+ }
+ }
+ }, rootNodeColors);
+
+ return serializeXmlWithoutIndentNode_(rootNodeColors);
+ },
+
+ /**
+ * Serializes an Colors to a attrs XML file.
+ */
+ colorToAttrsXmlString(artwork) {
+ let xmlAttrsDoc = document.implementation.createDocument(null, 'resources');
+ let rootNodeAttrs = xmlAttrsDoc.documentElement;
+ rootNodeAttrs.innerHTML += '\n';
+
+ artwork.walk((layer, parentNode) => {
+ if (layer instanceof PathLayer) {
+
+ if (layer.fillColor != null && layer.fillColor != '') {
+ let colorNode = xmlAttrsDoc.createElement('attr');
+ colorNode.setAttribute('name', layer.id + '_color');
+ colorNode.setAttribute('format', 'reference');
+ rootNodeAttrs.appendChild(colorNode);
+ rootNodeAttrs.innerHTML += '\n';
+ }
+
+ if (layer.strokeColor != null && layer.strokeColor != '') {
+ let strokeColorNode = xmlAttrsDoc.createElement('attr');
+ strokeColorNode.setAttribute('name', layer.id + '_stroke_color');
+ strokeColorNode.setAttribute('format', 'reference');
+ rootNodeAttrs.appendChild(strokeColorNode);
+ rootNodeAttrs.innerHTML += '\n';
+ }
+ }
+ }, rootNodeAttrs);
+
+ return serializeXmlWithoutIndentNode_(rootNodeAttrs);
+ },
+
+ /**
+ * Serializes an Colors to a styles XML file.
+ */
+ colorToStylesXmlString(artwork) {
+ let xmlStylesDoc = document.implementation.createDocument(null, 'resources');
+ let rootNodeStyles = xmlStylesDoc.documentElement;
+ rootNodeStyles.innerHTML = '\n\n';
+ let styleNode = xmlStylesDoc.createElement('style');
+ styleNode.setAttribute('name', 'AppTheme');
+ styleNode.setAttribute('parent', 'Theme.AppCompat.Light.DarkActionBar');
+ styleNode.append('\n');
+
+ let itemNode = xmlStylesDoc.createElement('item');
+ itemNode.setAttribute('name', 'colorPrimary');
+ itemNode.innerHTML = '@color/colorPrimary';
+ styleNode.appendChild(itemNode);
+ styleNode.append('\n');
+ itemNode = xmlStylesDoc.createElement('item');
+ itemNode.setAttribute('name', 'colorPrimaryDark');
+ itemNode.innerHTML = '@color/colorPrimaryDark';
+ styleNode.appendChild(itemNode);
+ styleNode.append('\n');
+ itemNode = xmlStylesDoc.createElement('item');
+ itemNode.setAttribute('name', 'colorAccent');
+ itemNode.innerHTML = '@color/colorAccent';
+ styleNode.appendChild(itemNode);
+ styleNode.append('\n');
+
+ artwork.walk((layer, parentNode) => {
+ if (layer instanceof PathLayer) {
+
+ if (layer.fillColor != null && layer.fillColor != '') {
+ let itemNode = xmlStylesDoc.createElement('item');
+ itemNode.setAttribute('name', layer.id + '_color');
+ itemNode.innerHTML = '@color/' + layer.id + '_color';
+ styleNode.appendChild(itemNode);
+ styleNode.append('\n');
+ }
+
+ if (layer.strokeColor != null && layer.strokeColor != '') {
+ let strokeItemNode = xmlStylesDoc.createElement('item');
+ strokeItemNode.setAttribute('name', layer.id + '_stroke_color');
+ strokeItemNode.innerHTML = '@color/' + layer.id + '_stroke_color';
+ styleNode.appendChild(strokeItemNode);
+ styleNode.append('\n');
+ }
+ }
+ }, styleNode);
+
+ rootNodeStyles.appendChild(styleNode);
+ rootNodeStyles.append('\n');
+
+ return serializeXmlWithoutIndentNode_(rootNodeStyles);
+ },
+
/**
* Serializes a given Artwork and Animation to an animatedvector drawable XML file.
*/
- artworkAnimationToAvdXmlString(artwork, animation) {
+ artworkAnimationToAvdXmlString(artwork, animation, withColorsAttrs) {
let xmlDoc = document.implementation.createDocument(null, 'animated-vector');
let rootNode = xmlDoc.documentElement;
+
rootNode.setAttributeNS(XMLNS_NS, 'xmlns:android', ANDROID_NS);
rootNode.setAttributeNS(XMLNS_NS, 'xmlns:aapt', AAPT_NS);
@@ -66,7 +202,7 @@ export const AvdSerializer = {
rootNode.appendChild(artworkContainerNode);
let artworkNode = xmlDoc.createElement('vector');
- AvdSerializer.artworkToXmlNode_(artwork, artworkNode, xmlDoc);
+ AvdSerializer.artworkToXmlNode_(artwork, artworkNode, xmlDoc, withColorsAttrs);
artworkContainerNode.appendChild(artworkNode);
// create animation nodes (one per layer)
@@ -112,7 +248,7 @@ export const AvdSerializer = {
conditionalAttr_(blockNode, 'android:valueFrom', block.fromValue);
conditionalAttr_(blockNode, 'android:valueTo', block.toValue);
conditionalAttr_(blockNode, 'android:valueType',
- animatableProperties[block.propertyName].animatorValueType);
+ animatableProperties[block.propertyName].animatorValueType);
conditionalAttr_(blockNode, 'android:interpolator', block.interpolator.androidRef);
blockContainerNode.appendChild(blockNode);
});
@@ -125,13 +261,13 @@ export const AvdSerializer = {
* Helper method that serializes an Artwork to a destinationNode in an xmlDoc.
* The destinationNode should be a node.
*/
- artworkToXmlNode_(artwork, destinationNode, xmlDoc) {
+ artworkToXmlNode_(artwork, destinationNode, xmlDoc, withColorsAttrs) {
destinationNode.setAttributeNS(XMLNS_NS, 'xmlns:android', ANDROID_NS);
destinationNode.setAttributeNS(ANDROID_NS, 'android:width', `${artwork.width}dp`);
destinationNode.setAttributeNS(ANDROID_NS, 'android:height', `${artwork.height}dp`);
destinationNode.setAttributeNS(ANDROID_NS, 'android:viewportWidth', `${artwork.width}`);
destinationNode.setAttributeNS(ANDROID_NS, 'android:viewportHeight', `${artwork.height}`);
- conditionalAttr(destinationNode, 'android:alpha', artwork.alpha, 1);
+ conditionalAttr_(destinationNode, 'android:alpha', artwork.alpha, 1);
artwork.walk((layer, parentNode) => {
if (layer instanceof Artwork) {
@@ -141,9 +277,13 @@ export const AvdSerializer = {
let node = xmlDoc.createElement('path');
conditionalAttr_(node, 'android:name', layer.id);
conditionalAttr_(node, 'android:pathData', layer.pathData.pathString);
- conditionalAttr_(node, 'android:fillColor', layer.fillColor, '');
+ conditionalAttr_(node, 'android:fillColor', withColorsAttrs ?
+ ((layer.fillColor == null || layer.fillColor == '') ?
+ '' : '?attr/' + layer.id + '_color') : layer.fillColor, '');
conditionalAttr_(node, 'android:fillAlpha', layer.fillAlpha, 1);
- conditionalAttr_(node, 'android:strokeColor', layer.strokeColor, '');
+ conditionalAttr_(node, 'android:strokeColor', withColorsAttrs ?
+ ((layer.strokeColor == null || layer.strokeColor == '') ?
+ '' : '?attr/' + layer.id + '_stroke_color') : layer.strokeColor, '');
conditionalAttr_(node, 'android:strokeAlpha', layer.strokeAlpha, 1);
conditionalAttr_(node, 'android:strokeWidth', layer.strokeWidth, 0);
conditionalAttr_(node, 'android:trimPathStart', layer.trimPathStart, 0);
@@ -151,9 +291,9 @@ export const AvdSerializer = {
conditionalAttr_(node, 'android:trimPathOffset', layer.trimPathOffset, 0);
conditionalAttr_(node, 'android:strokeLineCap', layer.strokeLinecap, DefaultValues.LINECAP);
conditionalAttr_(node, 'android:strokeLineJoin', layer.strokeLinejoin,
- DefaultValues.LINEJOIN);
+ DefaultValues.LINEJOIN);
conditionalAttr_(node, 'android:strokeMiterLimit', layer.strokeMiterLimit,
- DefaultValues.MITER_LIMIT);
+ DefaultValues.MITER_LIMIT);
parentNode.appendChild(node);
return parentNode;
@@ -179,4 +319,4 @@ export const AvdSerializer = {
}
}, destinationNode);
},
-};
+};
\ No newline at end of file