This guide is intended for future contributors and potential maintainers. It includes both common and technical information about the project. It's important to read this document before starting working with the project to understand how everything works in the app.
Refer to README for the project setup and build instructions.
Whenever you'd like to translate Telegram X to a new language, or improve some existing string, you should refer to the cloud Translations Platform. All changes there affect Telegram X without need in updates, and, in most cases, even without need in the app restart.
Telegram X has only English language embedded (strings.xml
). strings.xml
files in locale-specific folders contain only strings that are somewhat important to be available before the very first connection with Telegram servers could be established after the clean app installation.
Telegram X also has translation tools available inside the app: you can create, install, or edit existing localizations from .xml
files. These tools might be useful when creating a new translation and checking how it looks before the language would be available on the Translations Platform.
- Add new string to
app/src/main/res/values/strings.xml
file; - Do not edit
strings.xml
in locale-specific folders (values-ja
,values-it
,values-ru
, etc); - Import new
strings.xml
file to the Translations Platform and add new keys; - In case translated string should be accessible immediately after app installation (i.e. before the first connection with Telegram server could be established), it should be added to all locale-specific
strings.xml
files as well (which should be autogenerated & based on the latest string version on the platform). Such strings are usually used only on the intro screen & proxy settings (which might be useful for regions with Internet censorship), thus you most likely will never need them there; - Do not concatenate two strings manually. Add another string to
strings.xml
(for example, usingformat_
key prefix), which would contain the format for concatenation that will allow translators to customize it in any way. Then use it like:Lang.getString(R.string.format_example, Lang.getString(R.string.stringA), Lang.getString(R.string.stringB))
.
- Add two strings to
strings.xml
with preserved key suffixes_one
and_other
, i.e. with keysxExamples_one
(for singular form) andxExamples_other
(for plural form); %1$s
will contain the formatted counter. Format arguments are accessible as well:%2$s
,%3$s
, …;- Use
Lang.plural(R.string.xExamples, number, …)
to get the final form based on the number.
Relative date strings are a special form of strings used to display a difference between a specific time in the past and current time (server or local).
When adding new relative date string, you shall define strings with 8 or 10 suffixes, depending on which form you use, otherwise the build will intentionally fail.
Dates in future are not supported: string with _now
suffix will be used instead.
Following 4 string suffixes are used when allowDuration
argument is true
:
_now
, ordinary: less than 5 or 30 seconds ago (exact amount is passed through thejustNowSeconds
argument). e.g.seen just now
_seconds
, plural: less than a minute ago. Example:seen 30 seconds ago
_minutes
, plural: less than an hour. Example:seen 5 minutes ago
_hours
, plural: less than 4 hours. Example:seen 3 hours ago
These suffixes are always used:
_today
, ordinary: today with time in%1$s
. Example:seen today at 12:00 PM
_yesterday
, ordinary: yesterday, with time in%1$s
. Example:seen yesterday at 5:12 AM
_weekday
, ordinary: less than 7 days ago. Example:seen on Tue at 6:30 PM
_date
, ordinary: more than a week ago. Example:seen on 07.07 at 11:11 AM
Approximate form: with an amount of days, weeks, months, or years, if difference is more than 2 calendar days
_days
, plural: before yesterday, but less than 14 days ago. Example:joined 2 days ago
_weeks
, plural: more than 14 days ago. Example:joined 2 weeks ago
_months
, plural: more than 30 days ago. Example:joined 2 months ago
_years
, plural: more than a year ago. Example:joined 1 year ago
- Define 8 or 10 strings in
strings.xml
with the suffixes described above - Build project to refresh auto-generated strings resources
- Call
Lang.getRelativeDate
with the string without suffix.approximate
argument must correspond to the chosen form, otherwise Telegram X will crash when undefined form is used.
Whenever you display a relative date, it's important to keep it updated, especially when the difference between two dates is small.
To calculate amount of milliseconds until the next update use one of Lang.getNextRelativeDateUpdateMs
methods depending on which form you used. Then schedule the refresher action, for example, via Handler
's sendMessage
method, but do not forget to cancel it whenever you are sure it won't longer needed (e.g. corresponding view got destroyed, screen closed, etc).
Date format methods are available in Lang
class. Never use custom patterns when displaying dates, as different regions might have completely different preferred formats. Users must see them in a way they are used to. If app language differs from the system language, it should rely on the preferred format for the chosen locale.
Whenever you need some color, like with translations strings, you should never use hardcoded colors. You also shall register all theme update listeners for a smooth transition on any screen when theme switches automatically.
There're several built-in themes. Custom themes can be installed via exported .tgx-theme
files
Theme.getColor(R.id.theme_color_$colorId
, where $colorId
– desired theme color key.
This will return currently effective color: either from the effective theme, or an intermediate value when switching themes.
Theme.getColor
structure is designed in a heavily-optimized way so it could be called from methods such as onDraw
directly, without need in caching the value.
Defined theme colors and properties are located in colors-and-properties.xml file and built-in themes can be found in the same folder.
-- TODO
In Telegram X you shall forget about the usual way of animating View
or properties in Android and use one of the classes located in me.vkryl.android.animator
package. Main idea is to use as few animators as possible to avoid desync in choreography, improve animations performance and reduce battery usage.
Base class that animates float
value, which can be used to animate pretty much anything.
class Example implements FactorAnimator.Target, View.OnClickListener {
private static final int ANIMATOR_ROTATION = 0;
private static final int ANIMATOR_DISAPPEARANCE = 1;
private final FactorAnimator disappearAnimator = new FactorAnimator(ANIMATOR_DISAPPEARANCE, this, AnimatorUtils.OVERSHOOT_INTERPOLATOR, 200l);
// ...
@Override
public void onClick (View v) {
switch (v.getId()) {
case R.id.btn_showAnimated:
disappearAnimator.animateTo(1f);
break;
case R.id.btn_show:
disappearAnimator.forceFactor(1f);
break;
case R.id.btn_hideAnimated:
disappearAnimator.animateTo(0f);
break;
case R.id.btn_hide:
disappearAnimator.forceFactor(0f);
break;
case R.id.btn_rotate:
rotateAnimator.animateTo(MathUtils.random(0, 360));
break;
}
}
@Override
public void onFactorChanged (int id, float factor, float fraction, FactorAnimator callee) {
switch (id) {
case ANIMATOR_DISAPPEARANCE: {
final float scale = .7f + .3f * factor;
someView.setScaleX(scale);
someView.setScaleY(scale);
someView.setAlpha(MathUtils.clamp(factor));
break;
}
case ANIMATOR_ROTATION: {
someView.setRotation(factor);
break;
}
}
}
}
Simplified version of FactorAnimator
. Can be used as an animated boolean
replacement that gives boolean
itself and its current animated representation as float
value between 0.0
(false
) and 1.0
(true
). Because it was created much later than FactorAnimator
in most places, where FactorAnimator
is currently used, it should be replaced with BoolAnimator
.
In general, prefer using BoolAnimator
over FactorAnimator
, if you need to animate between just two states.
private final BoolAnimator isVisible = new BoolAnimator(0,
(id, factor, fraction, callee) -> {
someView.setAlpha(factor);
someOtherView.setAlpha(1f - factor);
},
AnimatorUtils.DECELERATE_INTERPOLATOR,
180L
);
//...
@Override
public void onClick (View v) {
switch (v.getId()) {
case R.id.btn_toggleVisibility:
isVisible.toggleValue(true);
break;
case R.id.btn_show:
isVisible.setValue(true, true);
break;
case R.id.btn_hide:
isVisible.setValue(false, true);
break;
}
}
Whenever you'd like to set value without animation (for example, when View
gets re-used, onBindViewHolder
gets called and you need to update view state without animations), just pass false
as the last argument in setValue
or toggleValue
methods.
Animator that is used to animate small lists of objects of T
type. Can be seen in action in the list of voters' avatars in public polls.
Note that in order ListAnimator
to work properly, T
type has to implement equals
and hashCode
methods.
class SomeObject {
public final long id;
public final int color;
public SomeObject (long id, int color) {
this.id = id;
this.color = color;
}
@Override
public boolean equals (Object obj) {
return obj instanceof SomeObject && ((SomeObject) obj).id == this.id;
}
@Override
public int hashCode () {
return (int) (id ^ (id >>> 32));
}
}
Then, whenever needed, simply pass list of items of T
type to ListAnimator<T>
, and all needed animations will be handled properly:
private static class SomeView extends View implements ListAnimator.Callback {
public SomeView (Context context) {
super(context);
}
private final ListAnimator<SomeObject> listAnimator = new ListAnimator<>((animator) -> {
invalidate();
});
public void setObjects (@Nullable List<SomeObject> items, boolean animated) {
listAnimator.reset(items, animated);
}
@Override
public void onDraw (@NonNull Canvas c) {
final int radius = Screen.dp(12f);
final int spacing = Screen.dp(4f);
for (int i = listAnimator.size() - 1; i >= 0; i--) {
ListAnimator.Entry<SomeObject> item = listAnimator.getEntry(index);
final float alpha = item.getVisibility();
final float x = radius + (radius * 2 + spacing) * item.getPosition();
final int color = ColorUtils.alphaColor(alpha, item.item.color);
c.drawCircle(x, radius + spacing, radius, Paints.fillingPaint(color));
}
}
}
getPosition
: returns animated item index, i.e. when item moves it will be betweenfromIndex
andtoIndex
.getVisibility
: returns animated visibility (alpha) from0
to1
.getIndex
: returns an actual item index in a list. Note that it can be outside of range of the last passed list in case of removed items.
Note: ListAnimator
implements Iterable
interface. However, you shall not use it inside drawing methods (onDraw
, draw
, etc) to avoid object allocation.
Pretty much the same as ListAnimator<T>
, but meant to be used for a single item. Useful for texts or buttons that should get replaced with animation.
Unlike most Android projects, Telegram X doesn't use regular Activity
or Fragment
navigation.
Instead, it's based on its own abstract BaseActivity
class, which uses NavigationController
, which manages navigation between ViewController<T>
's subclasses.
BaseActivity
is a common class that is responsible for:
- Creating and destroying
NavigationController
; - Managing keyboard open/close, window insets, etc;
- Managing window flags;
- Displaying and hiding passcode;
- Displaying and hiding pop-ups;
- Managing gestures (or, in fact, passing them to the target components).
There are currently only two BaseActivity
's subclasses:
ManageSpaceActivity
: previously used inside theandroid:manageSpaceActivity
application property, but for now unused;MainActivity
: main app entry point that handles pretty much everything. It's also responsible for creating and restoring navigation stack, syncing TDLib instances, switching accounts, handling deep links, etc.
NavigationController
is a main navigation component that responsible for navigation between ViewController
instances.
NavigationController
and ViewController
are designed the way animations would perform without any interruptions, frame drops, and it would be relatively easy to change gestures, and, for example, implement table/desktop layout in future.
Each ViewController
instance contains a reference to the root BaseActivity
and an optional Tdlib
instance. Tdlib
instances are managed automatically, so it's possible to have screens attached to different Tdlib
instances (accounts) at the same time.
Whenever you want to create a new screen, you'll need to:
- Add a unique controller identifier to
ids.xml
with thecontroller_
prefix; - Create a new class inside the
org.thunderdog.challegram.ui
package, inheritViewController<T>
or any of its children, and override needed methods that will describe theViewController
; - From the entry point, such as
onClick
,onTouchEvent
or wherever else, you have to create its instance, set arguments (optionally), and callNavigationController
'snavigateTo
method.
When you just create a ViewController
instance, no methods are called. After you pass it to NavigationController
's navigateTo
, setControllerAnimated
or other similar method, many things happen.
usePopupMode
: iftrue
, vertical navigation and gestures will be used for this screen;onPrepareToShow
: gets called every time beforeViewController
contents become visible (including backward navigation);onCreateView
: gets called just once whenViewController
'sget
method gets called for the first time;onFocus
: gets called wheneverViewController
got fully visible: all corresponding navigation transitions have finished, activity resumed, passcode unlocked;onBlur
: gets called wheneverViewController
lost "focus": navigation transition or gesture started, activity paused, passcode shown, etc;onActivityPause
,onActivityResume
: gets called when correspondingActivity
method got called. You most likely will never need it, as there areonFocus
andonBlur
methods;needAsynchronousAnimation
: whentrue
, navigation transition won't start immediately afterNavigationController
'snavigateTo
ornavigateBack
method got called. Instead, it'll wait util targetViewController
'sexecuteScheduledAnimation
method gets called, orgetAsynchronousAnimationTimeout
expires, whichever comes first. IfgetAsynchronousAnimationTimeout
returns negative or zero value, animation transition will wait forever untilexecuteScheduleAnimation
gets called. Note that this scheme applies to both forward and backward animation, so, after there's no longer any need in asynchronous animation,needAsynchronousAnimation
shall start returningfalse
;destroy
: gets called wheneverViewController
gets removed from navigation stack and won't be displayed any longer. Best place to clean all resources, views, etc.
ViewController<T>
: basic screen;TelegramViewController<T>
: based onViewController<T>
. Basic screen with built-in chat & messages search, which could be found, for example, when tapping search button on the main screen;RecyclerViewController<T>
: based onTelegramViewController<T>
. Basic screen withRecyclerView
;EditBaseController<T>
: based onViewController<T>
. Screen with done button in the bottom-right corner, useful when you want to edit something (e.g. change username, rename contact, etc);ViewPagerController<T>
: based onTelegramViewController<T>
. Basic screen withViewPager
. Uses otherViewController
instances to display its tabs;WebkitController<T>
: based onViewController<T>
. Basic screen withWebView
;MapController<V extends View, T>
: based onViewController<MapController.Args>
. Screen used to display map with some information, such as venue, live location, etc. If you would like to add new maps implementation (e.g. OSM, Yandex, etc), you should inherit froom this class. SeeMapGoogleController
for implementation based on Google Maps;SharedBaseController<T extends MessageSourceProvider>
: based onViewController<SharedBaseController.Args>
. Inherit from this class, if you want to add a completely new profile tab that doesn't look like the others. You most likely will never need it, as there isSharedCommonController
for lists andSharedMediaController
for grids.
By default, when activity gets destroyed, navigation stack is not saved. In order to keep your ViewController
instance saved, you have to:
- Implement
saveInstanceState
method and returntrue
. Important: don't forget to usekeyPrefix
when storing data inside the bundle; - Implement
restoreInstanceState
method and returntrue
. Important: don't forget to usekeyPrefix
when accessing data previously stored insidesaveInstanceState
method; - Add instance creation in the
restoreController
method insideMainActivity
. Note that this step might be reworked later in a nicer way.
To test saving and restoring navigation stack you might want to use Don't keep activities toggle in Developer Options in the system settings.
There's a special navigation type in Telegram X: pseudo 3D-touch. It could be found when holding chats in the chats list (when Chat Previews
are enabled in Settings – Interface), holding photos and videos in shared media tab in profiles, and many other places.
Whenever you want to implement 3D-touch, target view shall inherit from BaseView
class. There are two modes in which BaseView
could be used:
BaseView
allows displaying any desired ViewController
:
BaseView view = new BaseView(context, null);
view.setOnClickListener(view -> {
// do anything
});
view.setCustomControllerProvider(new CustomControllerProvider() {
@Override
public boolean needsForceTouch (BaseView v, float x, float y) {
return true; // can be based on some setting
}
@Override
public boolean onSlideOff (BaseView v, float x, float y, @Nullable ViewController openPreview) {
return false; // gets called when user's finger gets moved outside of view range.
// return true, if preview should be closed at this point
}
@Override
public ViewController createForceTouchPreview (BaseView v, float x, float y) {
SomeController c = new SomeController(v.getContext(), tdlib);
c.setArguments(...);
return c;
}
});
On ViewController
side just check whether isInForceTouchMode
returns true
, and adjust the layout at the creation step, if needed.
BaseView
also has built-in utility methods that allow opening chat previews in even more easy way:
// tdlib reference here controls which account will be used for the chat preview
BaseView view = new BaseView(context, tdlib);
view.setOnClickListener((v) -> { /* do anything */ });
view.setPreviewChatId(chatList, chatId);
-- TODO
view.setPreviewActionListProvider(new BaseView.ActionListProvider() {
});
Telegram X heavily uses custom views with custom drawing methods to reduce layout structure complexity, easily implement animations of any kind, improve frame rate, etc.
For that reason, there're many utilties built for custom drawings.
-- TODO
-- TODO
-- TODO
Whenever you navigate to the screen with items that load from local storage, or you expect that data will get loaded relatively fast (let's say, less than 250ms), instead of starting animation immediately, wait until it gets loaded.
ViewController
allows doing so in a convenient way by overriding needAsynchronousAnimation
and calling executeScheduledAnimation
methods (see previous section for more details). In case of other animations (such as search bar opening, etc), you have to handle things manually and call animateTo
, setValue
or other animation-related method once content get prepared.
Sample scheme for chats list screen:
- Create screen;
- Calculate how many items need to be loaded to fit entire screen (with one or few extra items to enable scrolling);
- Load items from database;
If there are items available locally:
3. Update the list;
4. Call executeScheduledAnimation
;
5. Result: user doesn't see rudimentary progress bar and other related transitions, but instead sees chats list immediately once navigation animation starts.
If there are no items available locally:
3. Immediately start loading data from server. Instead of constant amount of items, load as many items as calculated at step #2 during the first request. This will help displaying the list faster, if calculated value is smaller than initially desired constant, and will avoid half-blank list, if screen size is too big (and constant value is too small, accordingly);
4. Call executeScheduledAnimation
5. Once data gets loaded, display content. Avoid updating layout more than once
6. Result: user sees progress bar, which is OK, because it's not possible to predict how much time the request will take for sure. Once items get loaded from server, list will display.
Unless working with heavily optimized custom ViewGroup
, you shall avoid layouts during animations to prevent unnecessary frame drops. Instead, use setTranslationX
, setTranslationY
, setAlpha
, setScaleX
, setScaleY
, or invalidate
in case of custom views.
- Never allocate any objects inside drawing methods, such as
onDraw
,draw
, etc. Make sure methods you call there don't allocate them too; - Whenever possible, allocate as few objects as possible and avoid heavy work while scrolling lists (e.g. when
onBindViewHolder
gets called). There are still a lot of lowend devices that benifit from such optimizations.
- Reuse animator instances (
FactorAnimator
,BoolAnimator
,ListAnimator
, etc) each time you want to animate the same item, since they handle animation cancellations and re-starts properly; - Think of animator and its callback as a choreographer: instead of creating multiple animator instances for each view property, use just one, and update all properties relevant to the transition at once when callback gets called.
There are few rules that Telegram X tries to follow:
- Never copy-paste public repositories, use git submodules
- Same with libraries: never copy-paste, always add as gradle dependency
- Only vector drawables. Viewport for icons (
viewportWidth
,viewportHeight
) must be 24x24. Desired display size can be controlled bywidth
andheight
properties and must be included in the file name suffix. - Double whitespace as tab symbol
- Whitespace before the method's parameters opening brace:
public static void main () { ... }
- Kotlin is OK for new modules outside of
org.thunderdog
(seeme.vkryl
package), as long as it is interoperable with Java code (or it's private)
This section contains a list of features that have being started, but not finished, and will have notes on how and what's done and what's not.
Main part is actually done, as there's TdlibDataSource
that can be passed to ExoPlayer
instance, the only thing left is to change the UI logic the way ExoPlayer
would be initialized without waiting for the file to finish downloading, like it does with music.
Major part of Instant View 2.0 blocks is supported. Remaining tasks:
- Tables: see
PageBlockTable
- Autosize embeds: bugfixes to avoid scroll jumping, cache height when view gets recycled, etc
- Anchor support inside details block
- Follow/unfollow button in channel link
- View counter on the bottom
- Refresh chat member count in chat link automatically
- Problem with the pre block background
Wrong layout?
button
This is just the list of thoughts on some existing things that could be done better or just what could be done in future, which are not meat to be or not to be done. There usually just no time to work on them, as users want features, more frequent updates, and less bugs, they completely do not care about project structure and other internal stuff. Order in this list does not mean anything.
To allow transparent headers and other redesigns. To ease up the transition to full-screen ViewController
layout, it could be used only when some allowRunningInFullScreen
method returns true
(to be removed later when all screens will migrate to a new layout).
This could be done relatively easy by changing the way NavigationLayout
applies top offset to its children.
Initially, HeaderView
and its animations were designed to handle two header height: normal (like almost on all screens) and expanded (like on profile page).
However, later it became pretty obvious that header should be able to handle properly any height desired by ViewController
and be more dynamic, allowing making any changes to it without any difficulties.
It's obvious that project of this size should be split into smaller projects (modules) to allow multiple developers work on separate things independently.
However, as the of the moment of writing this text, there's only one project developer and currently all major modules, that in perfect world would be completely independent, intersect with each other and are not easy to separate at this time.
I've started separating most common utility classes and methods to me.vkryl
package that do not refer to any other packages (except for org.drinkless
in me.vkryl.td
package), but still the most part of the app is designed as it is.
As I see, in perfect world Telegram X would be separated in a way similar to AndroidX: as a tree of packages with a structure similar to the roots of me.vkryl
package.
Here're some of the modules / packages that could be done in future:
theme
: handles everything related to colors and themes;language
: handles everything related to languages;text
: similar toorg.thunderdog.challegram.util.text
, but without explicit references toTdApi
,Tdlib
, otherorg.thunderdog.challegram.*
packages and other things irrelevant toText
that could be made abstract;leveldb
: Telegram X's java layer for working with LevelDB (seeorg.thunderdog.challegram.tool.LevelDB
);telegram
: similar toorg.thunderdog.challegram.telegram
, handling everything related toTDLib
: instances, caching objects, etc;navigation
: all navigation-related andViewController
logic, that could be very useful for any other non-Telegram-related projects.
In the beginning, many things that currently look like contexts previously were singletons. However, there are still some things that could be changed to context. Some of them:
- Themes. Like passing different
Tdlib
instances toViewController
allows creating screens with different accounts, it would be nice to be able to pass theme information as well, which would allow creating screens with different themes at the same time; - Audio player (
TGPlayerController
). While it may look like a context inside, it's still designed as a singleton, as it's the only place where audio playback listeners get registered and the only place it gets created isTdlibManager
. Making it more flexible would allow creating two players at the same time, i.e. to allow playing voice messages without destroying music player: putting audio on pause while voice messages play, and resuming back once they finish playing.
Currently Telegram X manages audio service and notification. However, it might be a good idea to get rid of what's possible on Telegram X side, and give full control to ExoPlayer. See Playback Notifications with ExoPlayer.
There're some java objects in Telegram X that are designed in a pretty common way (example without thread safety):
final class Example {
private int mNativeHandle;
public Example () {
mNativeHandle = newNativeObject();
}
public void release () {
if (mNativeHandle != 0) {
nativeRelase(mNativeHandle);
mNativeHandle = 0;
}
}
@Override
protected void finalize () throws Throwable {
try {
release();
} finally {
super.finalize();
}
}
private native static long newNativeObject ();
private native static void nativeRelease (long ptr);
}
However, according to this Google I/O talk, such objects have to be reworked.
Telegram X currently doesn't support accessibility features, because it heavily uses gestures and custom views that did not take them in mind from the beginning. However, at some point in future, it's obvious that they have to be implemented.
Currently Telegram X almost does not use any system integration features. A lot of related features could be added, but it's important to note that any external feature must not expose user identifiers and phone numbers to the system and, especially, to other apps.
When working with user identifiers, it's possible to encrypt them with some local encryption key accessible only to Telegram X, or store local_user_id <-> user_id
pair in key-value storage that is private to the app too.
Initially, Text
was designed to handle RTL
texts properly, but according to reports, it has been partially broken after one of the updates with nested entities support.
Currently when you tap on a downloaded video file, it opens in system app, which might not support the codec, or be missing at all. Instead, it would be better to use existing MediaViewController
and open video document with animation.
It's better to load maximum photo size for the given viewport size, and display it with subsampling (refresh tile while zooming and moving), e.g. via implementation similar to subsampling scale image view.
Like with Emoji Set setting, it's possible to add an ability to change icon set, see SettingsCloudIconController
.
There could be two formats:
- Single big xml or json file with svg data for each overriden icon
- Archive with svg files
The first one is more preferable, as the second one is more dangerous, since there could be a danger of zip-bombs, if not implemented properly (given custom icon pack files would be allowed), etc.
Some custom-icon-set.tgx-icons
file could look like:
<xml>
<icons>
<icon name="baseline_group">
<pathData>...</pathData>
<pathData>...</pathData>
<fillColor>#FF000000</fillColor> <!-- optional -->
</icon>
<!-- ... -->
</icons>
</xml>
or, in JSON:
{
"icons": [
{
"name": "baseline_group",
"data": [
{"pathData": "..."},
{"pathData": "..."},
{"fillColor": "#FF000000"} // Optional
]
}
]
}
Or, in completely custom format:
@baseline_group
..some path data..
..some path data..
#FF000000 // Optional
@@
@another_icon
...
@@
Since viewportWidth
and viewportHeight
have to be 24x24
, overriden svg instruction set could be applied to all baseline_group
display resolutions: baseline_group_16.xml
, baseline_group_24.xml
, baseline_group_56.xml
, baseline_group_96.xml
, etc.
Based on these icon requirements, it would be also pretty easy to implement in-app interface to view and edit all icons and see them immediately in action.
There are currently few icons that do not follow 24x24
format properly, and have to be reworked.