Автоматическая генерация ресурсов для приложений Android с помощью скриптов для Adobe Photoshop

в 0:31, , рубрики: Без рубрики

Предисловие

При разработке для Android, как известно, нужно закладываться на то, чтобы создавать ресурсы подо все возможные пиксельные плотности. Изначально таких плотностей было только три: ldpi, mdpi и hdpi. Однако прогресс на месте не стоит: пиксельные плотности экранов растут до невменяемых значений, а Google тем временем тихой сапой приставляли буковку «x» и уже добрались до xxxhdpi, получив в итоге целых шесть основных конфигураций экрана. Это означает, что если играть по правилам, то нужно сохранять полдесятка ресурсов на одну иконку. Но и это еще не все, ведь некоторые ресурсы имеют несколько различных состояний. Кнопки на панели действий (action bar) имеют два состояния, и это еще куда ни шло, но обычные кнопки их имеют куда больше.

Выходов несколько: можно замучить художника, можно плюнуть на поддержку многих плотностей и надеяться, что система их как-нибудь сама отмасштабирует, а можно воспользоваться тем, что программисты любят делать больше всего: автоматизацией. Есть разные инструменты, которыми можно осуществить это дело. Самым продвинутым, наверное, является Android Asset Studio. Это очень толковый инструмент, но, разумеется, иконки там рисуются только для стандартных случаев, а, если нужно сделать кнопки со своими уникальными стилями, он тут нам не помощник. И вот тут нас выручит поддержка скриптов в небезызвестном инструменте: Adobe Photoshop. Ради того, чтобы упростить весь процесс, ваш покорный слуга для себя написал несколько инструментов на подобный случай и теперь делится ими с читателями. Как их использовать, и как они работают, я и описываю далее. Все исходники лежат на BitBucket, а здесь я расскажу основные моменты и покажу некоторые хитрости работы со скриптами Photoshop, которые могут быть неочевидны начинающим. На всякий случай отмечу, что писал их для Photoshop CS6.

Использование скриптов и принципы работы

Перед тем, как рассказывать о скриптах, стоит дать ссылку на статью предшественника, в которой объясняется общий процесс написания скриптов для Photoshop. Если вкратце, то стандартный инструмент для этого дела — это ExtendScript Toolkit, распространяемый вместе с самим графическим редактором. Должен скорбно отметить, что абсолютно согласен с автором вышеприведенной статьи в том, что редактор действительно довольно бестолковый. Но уж какой есть, такой есть. Непосредственно писать скрипты можно, конечно, и не в нем, но для отладки придется с ним подружиться. Есть в нем на кнопке F1 и документация по встроенным в Photoshop функциям, которая такая же неудобная, как и сам редактор, но хотя бы выполняет свою основную функцию. Сами скрипты можно писать на разных языках, а сам я использовал JavaScript.

Adobe ExtendScript Toolkit

Создание иконки для всех плотностей

Возвращаясь к нашим баранам, все написанные скрипты для работы с ресурсами можно поделить на две части: одни из них непосредственно запускают исполнение нужных действий (все они начинаются со слова «Make»), а другие выполняют роль библиотек с функциями. Самый важный и универсальный инструмент — это MakeForAllDensities, который делает то, что и предполагает название: создает из одного документа ресурсы для всех плотностей. К документу есть некоторые требования:

  1. Документ должен быть создан для пиксельной плотности mdpi. Она принимается за базовую и потом масштабируется до нужных размеров.
  2. Документ должен уже быть где-нибудь сохранен, чтобы скрипт правильно прочитал название файла (а также определил, не nine-patch ли это, по приставке ".9"). Лучше всего сохранять в подпапке корневой папки проекта Android, тогда скрипт сам найдет папку res.
  3. Дополнительное требование: если это nine-patch, то тогда линии должны быть нарисованы на отдельном слое, самом нижнем.
  4. Ну и, разумеется, изображение должно быть векторное, а не растровое, иначе нет особого смысла в масштабировании его средствами Photoshop, а не Android. Есть одно исключение: к линиям nine-patch это не относится, и они могут быть и растровыми.

Если все эти требования соблюдены, то остальное уже дело техники: открываем документ в Photoshop и запускам скрипт по двойному щелчку. После запуска скрипт попросит указать папку с ресурсами (res), а если документ сохранен в подпапке проекта, то и сам сообразит, где сохранять, и дальше все остальное уже сделает сам.

Сам скрипт выглядит очень просто:

//@include ResizingAndSaving.jsx

#target photoshop
    
var outputFolder = detectFolder();
if (outputFolder) saveForAllDensities(outputFolder, 0, "");

Типичный JavaScript, за исключением странных первых двух строчек. Первая странная строчка похожа на обычный комментарий, который на самом деле никакой не комментарий, а импорт другого файла с нужными нами функциями. Это хитрость номер один, потому что в стандартном JavaScript такая фишка не катит. Вторая странная строчка, как можно легко угадать, подсказывает, что скрипт должен запускаться в Photoshop. Что делают остальные строчки, нам откроет ResizingAndSaving.jsx.

detectFolder здесь не привожу, так как ничего особенного в ней нет: функция проверяет, есть ли в надпапке папки документа папка res, и возвращает ее, если находит, а если нет, то спрашивает пользователя. А вот дальше начинается более важная часть скрипта.

function saveForAllDensities(outputFolder, version, postfix, ninePatchLines) {
	if (!ninePatchLines) ninePatchLines = computeNinePatchLines();
	
	var versionStr = version ?  "-v" + version : "";
	saveInFolder(outputFolder, "drawable-mdpi" + versionStr, 100, postfix, ninePatchLines);
	saveInFolder(outputFolder, "drawable-hdpi" + versionStr, 150, postfix, ninePatchLines);
	saveInFolder(outputFolder, "drawable-xhdpi" + versionStr, 200, postfix, ninePatchLines);
	saveInFolder(outputFolder, "drawable-xxhdpi" + versionStr, 300, postfix, ninePatchLines);
	saveInFolder(outputFolder, "drawable-xxxhdpi" + versionStr, 400, postfix, ninePatchLines);
}

Заранее отвечаю на вопрос, если он у кого-то возник: ldpi здесь нет, поскольку Google не рекомендуют для нее создавать ресурсы специально. Как уже было сказано ранее, файл может оказаться nine-patch, что в плане редактирования файла означает, что у него есть отдельный слой с черными линиями по краям. И эти линии нельзя просто брать и масштабировать: нужно обязательно закрашивать пиксели полностью черным цветом, либо не закрашивать вовсе, и на соседние пиксели залезать нельзя. Кроме того, нужно учитывать, что линии могут быть и не сплошные. Вот тут и вступает в дело функция computeNinePatchLines.

function computeNinePatchLines() {
	var docName = getDocName(false);
	if (!isNinePatch(docName)) return null;
	
	var ninePatchLines = null;
	
	var doc = app.activeDocument;
	var areaCheckingFunctions = [
		function(pos) {return areaIsEmpty(doc, pos, 0);},
		function(pos) {return areaIsEmpty(doc, 0, pos);},
		function(pos) {return areaIsEmpty(doc, pos, doc.height - 1);},
		function(pos) {return areaIsEmpty(doc, doc.width - 1, pos);}
	];
	maxPositions = [doc.width, doc.height, doc.width, doc.height];
	ninePatchLines = new Array();
	for (var pos = 0; pos < areaCheckingFunctions.length; pos++) {
		ninePatchLines.push(findLines(maxPositions[pos], areaCheckingFunctions[pos]));
	}

	return ninePatchLines;
}

Основной смысл этой функции в том, чтобы пройтись по каждой стороне картинки и проверить, где там затаились черные линии. После того, как они будут найдены, можно уже будет безо всяких премудростей умножать их длину на нужный коэффициент и дальше закрашивать нужную область как полагается. Интересно будет заглянуть в функцию areaIsEmpty:

function areaIsEmpty(doc, x, y) {
   var state = getState();
   
	if (doc.colorSamplers.length == 0) {
		var colorSampler = doc.colorSamplers.add([x,y]);
	} else {
		var colorSampler = doc.colorSamplers[0];
		colorSampler.move([x, y]);
	}

	var areaEmpty;
	try {
		areaEmpty = colorSampler.color.rgb.hexValue !== "000000";
	} catch (e) {
		areaEmpty = true;
	}

	restoreState(state);
	
	return areaEmpty;
}

Эта функция призвана проверять, закрашен ли пиксель черным цветом или нет. Только тут дело в том, что в Photoshop, как оказалось, средствами стандартного API не получается проверить, пуст ли определенный пиксель, не говоря уже о том, чтобы просто узнать его цвет. Поэтому приходится ставить на него сэмплер цвета и смотреть, выдаст ли он исключение при проверке цвета или нет. Если выдал, значит, пиксель пуст. Если нет, то можно смотреть его цвет. В этом состоит хитрость номер два. Функция findLines, которую я здесь не привожу, попросту применяет areaIsEmpty подряд для всех пикселей по одной из четырех границ экрана и записывает их позиции.

Дальше можно масштабировать ресурсы и сохранять их в папку.

function saveInFolder(outputFolder, subFolder, scaling, postfix, ninePatchLines) {
	var opts = new ExportOptionsSaveForWeb(); 
	opts.format = SaveDocumentType.PNG; 
	opts.PNG8 = false; 
	opts.transparency = true; 
	opts.quality = 100;
	
	var state = getState();
	
	if (ninePatchLines) {
		var doc = app.activeDocument;	
		doc.resizeCanvas(doc.width - 2, doc.height - 2);
		resize(scaling, true);
		doc.resizeCanvas(doc.width + 2, doc.height + 2);
		drawLines(doc, scaling / 100, ninePatchLines);
	} else {
		resize(scaling, true);
	}
	activeDocument.exportDocument(createFile(outputFolder, subFolder, postfix, ".png", false), ExportType.SAVEFORWEB, opts);
	restoreState(state);
}

Тут все, в принципе, очевидно, но вот то, как делается непосредственно изменение размеров картинки, заслуживает отдельного пояснения. Казалось бы, есть функция Document.resizeImage, и нужно просто вызывать ее, так? А вот ничего и не выйдет: стили слоев при этом не масштабируются. Можно записать действие и потом его проигрывать программно. Этот вариант работает, но он плох тем, что тогда к скрипту обязательно нужно прилагать библиотеку этих самых действий, которую нужно импортировать перед запуском, что как-то не очень удобно.

Другой вариант — это воспользоваться инструментом, который описывал мой уже упомянутый предшественник, а именно ScriptListener.8li. Этот инструмент позволяет записать все действия, которые совершаются в Photoshop, в скрипт, даже если эти действия не поддерживаются в стандартном API. Скрипты на выходе вылезают, конечно, довольно невнятные, но свою задачу выполняют на отлично. При некотором старании можно понять, какие параметры за что отвечают, и сделать из записанных конкретных действий функцию. Именно этим способом и появилась вот такая вот малоразборчивая, но работоспособная функция:

function resize(size, relative) {
	var idImgS = charIDToTypeID( "ImgS" );
		var desc = new ActionDescriptor();
		var idWdth = charIDToTypeID( "Wdth" );
		var idPxl = charIDToTypeID( relative ? "#Prc" : "#Pxl" );
		desc.putUnitDouble( idWdth, idPxl, size );
		var idscaleStyles = stringIDToTypeID( "scaleStyles" );
		desc.putBoolean( idscaleStyles, true );
		var idCnsP = charIDToTypeID( "CnsP" );
		desc.putBoolean( idCnsP, true );
		var idIntr = charIDToTypeID( "Intr" );
		var idIntp = charIDToTypeID( "Intp" );
		var idbicubicSharper = stringIDToTypeID( "bicubicAutomatic" );
		desc.putEnumerated( idIntr, idIntp, idbicubicSharper );
	executeAction( idImgS, desc, DialogModes.NO );
}

Это и была хитрость номер три. После того, как нужные размеры изображения получены, скрипт рисует, если нужно, линии nine-patch, и новоиспеченный ресурс отправляется в необходимую папку.

Создание иконок для панели действий

Кроме MakeForAllDensities есть еще четыре скрипта MakeActionBarIcons, которые делают иконки для панели действий: для черной и белой темы, отключаемые и неотключаемые. Используются они так же, как и MakeForAllDensities, за тем исключением, что теперь документ должен содержать только один слой. В этом слое важно только соблюдать форму иконки, а стили скрипт применит сам.

Теперь трудность состоит в том, что у Google есть определенные требования к стилю иконок в зависимости от их состояния. Если иконка существует только в одном состоянии, то тут все просто, но если ее нужно отключать, то вот тут уже надо придумывать, как модифицировать вид слоев программно. Для иконок на панель действий нам нужно знать, как менять прозрачность слоя и его цвет. С первым проблем нет, а вот в отношении последнего стандартный API опять дает слабину. Значит, придется вновь обращаться к спасительному ScriptListener.8li. В результате его применения в файле Styles появилась функция, которая и поможет нам поменять цвет векторного слоя: setLayerColor. Ту тарабарщину, которая находится в теле функции, я, пожалуй, опущу.

В принципе, для иконок панели действий ничего, кроме вышеописанного, и не нужно. Но те, кто уже глянули в файл Styles, заметили, что там лежит еще множество функций, полученных с помощью ScriptListener.8li, которые могут манипулировать эффектами слоев. Писались они для моих собственных иконок, скрипты для создания которых я в репозитарий уже не добавляю. По этой причине существующих функций, разумеется, может кому-то оказаться и недостаточно, и нужно будет сделать свои. Опять же, можно либо записать действия, либо сделать стили и применять их программно. Но это неудобно, поэтому лучше опять обойтись функциями, благо ScriptListener.8li мы уже освоили. И вот тут обнаружится очередная загвоздка: если записать скрипт для добавления определенного эффекта слоя и оформить его в функцию, то при ее применении уже установленные эффекты будут пропадать. Здесь пригождается хитрость номер четыре. Если обратить внимание на начало той белиберды, которую выдает ScriptListener.8li для каждого применения эффекта, везде будут аналогичные следующим строчки:

var idsetd = charIDToTypeID( "setd" );
	var desc = new ActionDescriptor();
	var idnull = charIDToTypeID( "null" );
		var ref = new ActionReference();
		var idPrpr = charIDToTypeID( "Prpr" );
		var idLefx = charIDToTypeID( "Lefx" );
		ref.putProperty( idPrpr, idLefx );
		var idLyr = charIDToTypeID( "Lyr " );
		var idOrdn = charIDToTypeID( "Ordn" );
		var idTrgt = charIDToTypeID( "Trgt" );
		ref.putEnumerated( idLyr, idOrdn, idTrgt );
	desc.putReference( idnull, ref );
	var idT = charIDToTypeID( "T   " );
		var desc2 = new ActionDescriptor();
		var idScl = charIDToTypeID( "Scl " );
		var idPrc = charIDToTypeID( "#Prc" );
		desc2.putUnitDouble( idScl, idPrc, 100.000000 );

И в самом конце скрипта все завершается вот таким аккордом:

var idLefx = charIDToTypeID( "Lefx" );
desc.putObject( idT, idLefx, desc2 );
executeAction( idsetd, desc, DialogModes.NO );

За создание непосредственно стиля слоя отвечают четыре последних строки из первой порции кода выше, которые создают desc2 и устанавливают масштаб для стиля. Все остальное — это как раз и есть применение стиля. Теперь, когда мы знаем, какие строчки что делают, можно отделить жилки от мяса и вычленить ту самую часть кода, которая применяет непосредственно эффект. Повторяющиеся участки оформлены в отдельные функции в Styles, которые применяются подобным образом:

var style = newStyle(); //Создаем новый стиль
addColorOverlay(style, 0xFF, 0xFF, 0xFF, 100); //Применяем эффект
addStroke(style, 0x00, 0x00, 0x00, 3); //Применяем еще один эффект
applyStyle(style); //И, наконец, применяем созданный стиль

Теперь у нас есть весь инструментарий в руках, осталось его использовать. Напоминаю, что все эти чудодейственные средства затевались для того, чтобы применить разные стили к ресурсу в зависимости от состояния. Функция makeIcons в MenuIcons именно это и делает: применяет разные стили к иконке для панели действий и сохраняет получившееся. Я привожу здесь основной ее кусок.

if (makeStateful) {
	var selectorData = [
		{
			state_enabled: false,
			postfix: "disabled"
		},
		{
			postfix: "normal"
		}
	];
	makeSelectorXml(selectorData, outputFolder, "drawable");
}

var styleFunctions = [function(style) {applyActionBarItemStyle(whiteTheme, false)}];
var postfixes = ["normal"];
if (makeStateful) {
	styleFunctions.unshift(function(style) {applyActionBarItemStyle(whiteTheme, true)});
	postfixes.unshift("disabled");
}
saveStyledDrawables(outputFolder, styleFunctions, postfixes);

Первая часть функции создает селектор для нашего ресурса. Селекторы были описаны в не так давно опубликованной статье Тайны кнопок в Android. В нашем случае для панели действий создаются два состояния: когда кнопка выключена, и когда она в нормальном состоянии. Соответственно, в функцию makeSelectorXml передается массив из объектов, описывающих состояния. Объекты имеют поле postfix и, если нужно, одно или несколько полей, начинающихся со «state_». После этого makeSelectorXml делает из этого чуда-юда XML-файл селектора, который отправляется в папку drawables.

Вторая часть функции создает два массива: один содержит функции, которые применяют стили для данного состояния, а во втором массиве находятся соответствующие состояниям постфиксы. Каждая функция по применению стилей получает в свое полное распоряжение аргумент style. Это тот самый объект, который вылезает на выходе из newStyle, над которой мы бились не так давно. applyStyle вызывать в этих функциях не нужно, обо всем позаботится функция saveStyledDrawables. Функции makeSelectorXml и saveStyledDrawables приводить, думаю, не стоит, потому как там самый обычный, скучный JavaScript.

Заключение

Вот таким образом можно не рисовать вручную тучу иконок, а воспользоваться готовым решением и настрогать все из одной картинки. Можно, конечно, задействовать и Android Asset Studio, но в подходе со скриптами есть свои плюсы. Во-первых, можно делать скрипты для своих собственных кнопок, стилей которых в чужом инструменте попросту нет (почему, собственно, я и решил писать все это дело). Во-вторых, все-таки выгружать файл на сайт, колдовать с настройками, а потом качать и распихивать полученные файлы по папкам — это не так просто, как сделать двойной щелчок по скрипту, чтобы все сразу получилось как надо. Кроме того, Android Asset Studio не желает работать с PSD напрямую (по крайней мере, на момент написания статьи), не отличает nine-patch от обычной иконки, да и файлы в нем массово не обработаешь.

Надеюсь, что статья оказалась полезной как тем, кто занимается созданием приложений на Android, так и тем, кто хочет освоить написание скриптов для Photoshop.

Полезные ссылки

Автор: DrMetallius

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js