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