LoginSignup
1
1

More than 5 years have passed since last update.

Alloy で AttributedString を使えるようにする

Posted at

Titanium SDK 3.2.0.GA から追加された AttributedString を Alloy のビューとスタイルのみで使えるようにする、Alloy 自体の改造になります。

Titanium SDK 3.6 系から AttributedString が Android でもサポートされ、ネームスペースも Ti.UI.iOS.AttributedString から Ti.UI.AttributedString へ変更されるため、Alloy 自体の対応は 1.6 系からとなるようです(待てません。というか、諸事情で SDK と Alloy のバージョンを動かせない場合もありますし)。

そもそもコントローラに Ti.UI.iOS.createAttributedStinrg を直書きすれば良いのですが、Alloy を使っていて今更そんなこともしたくないですし。。。

というわけで、Alloy のコンパイラに手を入れて、ビューとスタイルのみで AttributedString を使えるようにしたいというわけです。実際にこれを適用すると、ビューとスタイルは以下のように記述できるようになります。

index.xml

<Alloy>
    <Window>
        <Label>
            <AttributedString class="as">
                Bacon ipsum dolor Appcelerator Titanium rocks! sit amet fatback leberkas salami sausage tongue strip steak.
            </AttributedString>
        </Label>
        <TextField>
            <AttributedString class="as">
                Bacon ipsum dolor Appcelerator Titanium rocks! sit amet fatback leberkas salami sausage tongue strip steak.
            </AttributedString>
            <AttributedHintText class="as">
                Bacon ipsum dolor Appcelerator Titanium rocks! sit amet fatback leberkas salami sausage tongue strip steak.
            </AttributedHintText>
        </TextField>
        <TextArea>
            <AttributedString class="as">
                Bacon ipsum dolor Appcelerator Titanium rocks! sit amet fatback leberkas salami sausage tongue strip steak.
            </AttributedString>
        </TextArea>
    </Window>
</Alloy>

index.tss

"Window": {
    backgroundColor: '#fff',
    layout: 'vertical'
},
"Label": {
    top: 20,
    right: 10,
    left: 10,
    width: Ti.UI.FILL,
    height: Ti.UI.SIZE
},
"TextField": {
    top: 20,
    right: 10,
    left: 10,
    width: Ti.UI.FILL,
    height: 44
},
"TextArea": {
    top: 20,
    right: 10,
    left: 10,
    width: Ti.UI.FILL,
    height: 200
},
".as": {
    attributes: [
        {
            type: Ti.UI.ATTRIBUTE_UNDERLINES_STYLE,
            value: Ti.UI.ATTRIBUTE_UNDERLINE_STYLE_SINGLE,
            range: [0, 107]
        },
        {
            type: Ti.UI.ATTRIBUTE_BACKGROUND_COLOR,
            value: "red",
            range: [18, 12]
        },
        {
            type: Ti.UI.ATTRIBUTE_BACKGROUND_COLOR,
            value: "blue",
            range: [31, 2]
        },
        {
            type: Ti.UI.ATTRIBUTE_BACKGROUND_COLOR,
            value: "yellow",
            range: [40, 6]
        },
        {
            type: Ti.UI.ATTRIBUTE_FOREGROUND_COLOR,
            value: "orange",
            range: [0, 107]
        },
        {
            type: Ti.UI.ATTRIBUTE_FOREGROUND_COLOR,
            value: "black",
            range: [40, 6]
        }
    ]
}

それではインストールされた Alloy のフォルダへ移動して、以下の改造を適用していきます。

alloy/Alloy/common/constants.js

186 行目にある AdView の下辺りに、以下の 1 行を挿入します。

AttributedString: NS_TI_UI_IOS,

alloy/Alloy/commands/compile/parsers/Ti.UI.iOS.AttributedString

AttributedString 用に以下のコードを新規に設置します。

var _ = require('../../../lib/alloy/underscore'),
    styler = require('../styler'),
    U = require('../../../utils'),
    CU = require('../compilerUtils'),
    tiapp = require('../../../tiapp');

var MIN_VERSION = '3.2.0';

exports.parse = function(node, state) {
    return require('./base').parse(node, state, parse);
};

function parse(node, state, args) {
    if (tiapp.version.lt(tiapp.getSdkVersion(), MIN_VERSION)) {
        U.die('Ti.UI.iOS.AttributedString (line ' + node.lineNumber + ') requires Titanium 3.2.0+');
    }

    // Get label text from node text, if present
    var nodeText = U.XML.getNodeText(node);
    if (nodeText) {
        if (U.isLocaleAlias(nodeText)) {
            state.extraStyle = {'text': styler.STYLE_EXPR_PREFIX + nodeText};
        } else {
            state.extraStyle = styler.createVariableStyle('text', "'" + U.trim(nodeText.replace(/'/g, "\\'")) + "'");
        }

        if (nodeText.match(/\{([^}]+)\}/) !== null) {
            state.extraStyle = nodeText;
        }
    }

    var nodeState = require('./default').parse(node, state);
    delete state.extraStyle;

    var code = nodeState.code;

    return {
        parent: {
            node: node,
            symbol: args.symbol
        },
        code: code
    };
}

alloy/Alloy/commands/compile/parsers/Ti.UI.Label

Ti.UI.LabelAttributedString が読み込まれるように以下のコードをコピペします。

var _ = require('../../../lib/alloy/underscore')._,
    styler = require('../styler'),
    CU = require('../compilerUtils'),
    U = require('../../../utils');

exports.parse = function(node, state) {
    return require('./base').parse(node, state, parse);
};

function parse(node, state, args) {
    var attributedStringsymbol,
        attributedStringObj = {},
        code = '';

    _.each(U.XML.getElementsFromNodes(node.childNodes), function(child){
        if (child.nodeName === 'AttributedString' && !child.hasAttribute('ns')) {
            child.setAttribute('ns', 'Ti.UI.iOS');
        }

        if (CU.validateNodeName(child, 'Ti.UI.iOS.AttributedString')) {
            code += CU.generateNodeExtended(child, state, {
                parent: {},
                post: function(node, state, args) {
                    attributedStringsymbol = state.parent.symbol;
                }
            });

            node.removeChild(child);
        }
    });

    if (attributedStringsymbol) {
        attributedStringObj = styler.createVariableStyle('attributedString', attributedStringsymbol);
    }

    // Get label text from node text, if present
    var nodeText = U.XML.getNodeText(node),
        textObj = {};
    if (nodeText) {
        if (U.isLocaleAlias(nodeText)) {
            textObj = {'text': styler.STYLE_EXPR_PREFIX + nodeText};
        } else {
            textObj = styler.createVariableStyle('text', "'" + U.trim(nodeText.replace(/'/g, "\\'")) + "'");
        }

        if (nodeText.match(/\{([^}]+)\}/) !== null) {
            textObj["text"] = nodeText;
        }
    }

    state.extraStyle = _.extend(attributedStringObj, textObj);

    var nodeState = require('./default').parse(node, state);
    code += nodeState.code;

    // Generate runtime code using default
    return _.extend(nodeState, {
        code: code
    });
}

alloy/Alloy/commands/compile/parsers/Ti.UI.TextField

Ti.UI.TextFieldAttributedStringAttributedHintText が読み込まれるように以下のコードをコピペします。

var _ = require('../../../lib/alloy/underscore')._,
    styler = require('../styler'),
    U = require('../../../utils'),
    CU = require('../compilerUtils'),
    CONST = require('../../../common/constants');

var KEYBOARD_TYPES = [
    'DEFAULT', 'ASCII', 'NUMBERS_PUNCTUATION', 'URL', 'EMAIL', 'DECIMAL_PAD', 'NAMEPHONE_PAD',
    'NUMBER_PAD', 'PHONE_PAD'
];
var RETURN_KEY_TYPES = [
    'DEFAULT', 'DONE', 'EMERGENCY_CALL', 'GO', 'GOOGLE', 'JOIN', 'NEXT', 'ROUTE',
    'SEARCH', 'SEND', 'YAHOO'
];
var AUTOCAPITALIZATION_TYPES = [
    'ALL', 'NONE', 'SENTENCES', 'WORDS'
];

exports.parse = function(node, state) {
    return require('./base').parse(node, state, parse);
};

function parse(node, state, args) {
    var code = '',
        postCode = '',
        extras = [],
        proxyProperties = {};

    // iterate through all children of the TextField
    _.each(U.XML.getElementsFromNodes(node.childNodes), function(child) {
        var fullname = CU.getNodeFullname(child),
            isProxyProperty = false,
            isControllerNode = false,
            isAttributedString = false,
            isAttributedHintText = false,
            hasUiNodes = false,
            controllerSymbol,
            parentSymbol;

        if (child.nodeName === 'AttributedString' && !child.hasAttribute('ns')) {
            child.setAttribute('ns', 'Ti.UI.iOS');
        }

        if (child.nodeName === 'AttributedHintText' && !child.hasAttribute('ns')) {
            child.nodeName = 'AttributedString';
            child.setAttribute('ns', 'Ti.UI.iOS');
            isAttributedHintText = true;
        }

        // validate the child element and determine if it's part of
        // the textfield or a proxy property assigment
        if (!CU.isNodeForCurrentPlatform(child)) {
            return;
        } else if (_.contains(CONST.CONTROLLER_NODES, fullname)) {
            isControllerNode = true;
        } else if (fullname.split('.')[0] === '_ProxyProperty') {
            isProxyProperty = true;
        } else if (CU.validateNodeName(child, 'Ti.UI.iOS.AttributedString')) {
            if (!isAttributedHintText) {
                isAttributedString = true;
            }
        }

        // generate the node
        code += CU.generateNodeExtended(child, state, {
            parent: {},
            post: function(node, state, args) {
                controllerSymbol = state.controller;
                parentSymbol = state.parent ? state.parent.symbol : state.item.symbol;
            }
        });

        // manually handle controller node proxy properties
        if (isControllerNode) {

            // set up any proxy properties at the top-level of the controller
            var inspect = CU.inspectRequireNode(child);
            _.each(_.uniq(inspect.names), function(name) {
                if (name.split('.')[0] === '_ProxyProperty') {
                    var prop = U.proxyPropertyNameFromFullname(name);
                    proxyProperties[prop] = controllerSymbol + '.getProxyPropertyEx("' + prop + '", {recurse:true})';
                } else {
                    hasUiNodes = true;
                }
            });
        }

        // generate code for proxy property assignments
        if (isProxyProperty) {
            proxyProperties[U.proxyPropertyNameFromFullname(fullname)] = parentSymbol;

        // generate code for the attribtuedString
        } else if (isAttributedString) {
            proxyProperties.attributedString = parentSymbol;
            node.removeChild(child);

        // generate code for the attribtuedHintText
        } else if (isAttributedHintText) {
            proxyProperties.attributedHintText = parentSymbol;
            node.removeChild(child);

        // generate code for the child components
        } else if (hasUiNodes || !isControllerNode) {
            postCode += '<%= parentSymbol %>.add(' + parentSymbol + ');';
        }

    });

    // support shortcuts for keyboard type, return key type, and autocapitalization
    var keyboardType = node.getAttribute('keyboardType');
    if (_.contains(KEYBOARD_TYPES, keyboardType.toUpperCase())) {
        node.setAttribute('keyboardType', 'Ti.UI.KEYBOARD_' + keyboardType.toUpperCase());
    }
    var returnKey = node.getAttribute('returnKeyType');
    if (_.contains(RETURN_KEY_TYPES, returnKey.toUpperCase())) {
        node.setAttribute('returnKeyType', 'Ti.UI.RETURNKEY_' + returnKey.toUpperCase());
    }
    var autocapitalization = node.getAttribute('keyboardType');
    if (_.contains(AUTOCAPITALIZATION_TYPES, autocapitalization.toUpperCase())) {
        node.setAttribute('autocapitalization', 'Ti.UI.TEXT_AUTOCAPITALIZATION_' + autocapitalization.toUpperCase());
    }


    // add all creation time properties to the state
    if (node.hasAttribute('clearOnEdit')) {
        var attr = node.getAttribute('clearOnEdit');
        extras.push(['clearOnEdit', attr === 'true']);
    }
    _.each(proxyProperties, function(v, k) {
        extras.push([k, v]);
    });
    if (extras.length) { state.extraStyle = styler.createVariableStyle(extras); }

    // generate the code for the textfield itself
    var nodeState = require('./default').parse(node, state);
    code += nodeState.code;

    // add the rows to the section
    if (nodeState.parent && postCode) {
        code += _.template(postCode, {
            parentSymbol: nodeState.parent.symbol
        });
    }

    // Update the parsing state
    return _.extend(state, {
        parent: nodeState.parent,
        code: code
    });
}

alloy/Alloy/commands/compile/parsers/Ti.UI.TextArea

Ti.UI.TextFieldAttributedString が読み込まれるように以下のコードをコピペします。

var _ = require('../../../lib/alloy/underscore')._,
    styler = require('../styler'),
    CU = require('../compilerUtils'),
    U = require('../../../utils'),
    CONST = require('../../../common/constants');

var KEYBOARD_TYPES = [
    'DEFAULT', 'ASCII', 'NUMBERS_PUNCTUATION', 'URL', 'EMAIL', 'DECIMAL_PAD', 'NAMEPHONE_PAD',
    'NUMBER_PAD', 'PHONE_PAD'
];
var RETURN_KEY_TYPES = [
    'DEFAULT', 'DONE', 'EMERGENCY_CALL', 'GO', 'GOOGLE', 'JOIN', 'NEXT', 'ROUTE',
    'SEARCH', 'SEND', 'YAHOO'
];
var AUTOCAPITALIZATION_TYPES = [
    'ALL', 'NONE', 'SENTENCES', 'WORDS'
];

exports.parse = function(node, state) {
    return require('./base').parse(node, state, parse);
};

function parse(node, state, args) {
    var code = '',
        postCode = '',
        extras = [],
        proxyProperties = {};

    // iterate through all children of the TextArea
    _.each(U.XML.getElementsFromNodes(node.childNodes), function(child) {
        var fullname = CU.getNodeFullname(child),
            isProxyProperty = false,
            isControllerNode = false,
            isAttributedString = false,
            hasUiNodes = false,
            controllerSymbol,
            parentSymbol;

        if (child.nodeName === 'AttributedString' && !child.hasAttribute('ns')) {
            child.setAttribute('ns', 'Ti.UI.iOS');
        }

        // validate the child element and determine if it's part of
        // the textarea or a proxy property assigment
        if (!CU.isNodeForCurrentPlatform(child)) {
            return;
        } else if (_.contains(CONST.CONTROLLER_NODES, fullname)) {
            isControllerNode = true;
        } else if (fullname.split('.')[0] === '_ProxyProperty') {
            isProxyProperty = true;
        } else if (CU.validateNodeName(child, 'Ti.UI.iOS.AttributedString')) {
            isAttributedString = true;
        }

        // generate the node
        code += CU.generateNodeExtended(child, state, {
            parent: {},
            post: function(node, state, args) {
                controllerSymbol = state.controller;
                parentSymbol = state.parent ? state.parent.symbol : null;
            }
        });

        // manually handle controller node proxy properties
        if (isControllerNode) {

            // set up any proxy properties at the top-level of the controller
            var inspect = CU.inspectRequireNode(child);
            _.each(_.uniq(inspect.names), function(name) {
                if (name.split('.')[0] === '_ProxyProperty') {
                    var prop = U.proxyPropertyNameFromFullname(name);
                    proxyProperties[prop] = controllerSymbol + '.getProxyPropertyEx("' + prop + '", {recurse:true})';
                } else {
                    hasUiNodes = true;
                }
            });
        }

        // generate code for proxy property assignments
        if (isProxyProperty) {
            proxyProperties[U.proxyPropertyNameFromFullname(fullname)] = parentSymbol;

        // generate code for the attribtuedString
        } else if (isAttributedString) {
            proxyProperties.attributedString = parentSymbol;
            node.removeChild(child);

        // generate code for the child components
        } else if (hasUiNodes || !isControllerNode) {
            postCode += '<%= parentSymbol %>.add(' + parentSymbol + ');';
        }

    });

    // support shortcuts for keyboard type, return key type, and autocapitalization
    var keyboardType = node.getAttribute('keyboardType');
    if (_.contains(KEYBOARD_TYPES, keyboardType.toUpperCase())) {
        node.setAttribute('keyboardType', 'Ti.UI.KEYBOARD_' + keyboardType.toUpperCase());
    }
    var returnKey = node.getAttribute('returnKeyType');
    if (_.contains(RETURN_KEY_TYPES, returnKey.toUpperCase())) {
        node.setAttribute('returnKeyType', 'Ti.UI.RETURNKEY_' + returnKey.toUpperCase());
    }
    var autocapitalization = node.getAttribute('keyboardType');
    if (_.contains(AUTOCAPITALIZATION_TYPES, autocapitalization.toUpperCase())) {
        node.setAttribute('autocapitalization', 'Ti.UI.TEXT_AUTOCAPITALIZATION_' + autocapitalization.toUpperCase());
    }


    // add all creation time properties to the state
    if (node.hasAttribute('clearOnEdit')) {
        var attr = node.getAttribute('clearOnEdit');
        extras.push(['clearOnEdit', attr === 'true']);
    }
    _.each(proxyProperties, function(v, k) {
        extras.push([k, v]);
    });
    if (extras.length) { state.extraStyle = styler.createVariableStyle(extras); }

    // generate the code for the textarea itself
    var nodeState = require('./default').parse(node, state);
    code += nodeState.code;

    // add the rows to the section
    if (nodeState.parent && postCode) {
        code += _.template(postCode, {
            parentSymbol: nodeState.parent.symbol
        });
    }

    // Update the parsing state
    return _.extend(state, {
        parent: nodeState.parent,
        code: code
    });
}

以上になります。特に、Ti.UI.TextField で hintText の色を変えたいとか、結構需要があったりしますよね。伝統芸能な、上にラベルをおいて change イベントを拾って。。。という実装もあるかと思いますが、attributedHintText を使ったほうが断然楽です。

注意事項

これは Alloy のコンパイラ自体に手を入れるため、自己責任でお願いします。ぼくの環境では問題なくコンパイルされますが、最悪 Alloy が壊れる可能性もあります。もちろん、Alloy 1.6 系で AttributedString がサポートされればこの改造は不要になります。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1