1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?