@maccesar/titools 2.2.11 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +255 -713
- package/bin/titools.js +1 -1
- package/lib/commands/update.js +5 -6
- package/lib/config.js +1 -0
- package/lib/downloader.js +10 -0
- package/package.json +1 -1
- package/skills/alloy-guides/SKILL.md +46 -48
- package/skills/alloy-guides/references/CONTROLLERS.md +4 -6
- package/skills/alloy-guides/references/MODELS.md +340 -184
- package/skills/alloy-guides/references/VIEWS_DYNAMIC.md +3 -4
- package/skills/alloy-guides/references/VIEWS_STYLES.md +44 -46
- package/skills/alloy-guides/references/VIEWS_WITHOUT_CONTROLLERS.md +4 -5
- package/skills/alloy-guides/references/VIEWS_XML.md +51 -17
- package/skills/alloy-guides/references/WIDGETS.md +1 -1
- package/skills/alloy-howtos/SKILL.md +12 -13
- package/skills/alloy-howtos/references/cli_reference.md +40 -41
- package/skills/alloy-howtos/references/samples.md +3 -4
- package/skills/purgetss/SKILL.md +369 -356
- package/skills/purgetss/references/EXAMPLES.md +24 -25
- package/skills/purgetss/references/animation-advanced.md +555 -0
- package/skills/purgetss/references/animation-system.md +395 -995
- package/skills/purgetss/references/apply-directive.md +111 -141
- package/skills/purgetss/references/arbitrary-values.md +206 -480
- package/skills/purgetss/references/class-categories.md +277 -0
- package/skills/purgetss/references/class-index.md +1 -420
- package/skills/purgetss/references/cli-commands.md +446 -556
- package/skills/purgetss/references/configurable-properties.md +163 -599
- package/skills/purgetss/references/custom-rules.md +33 -76
- package/skills/purgetss/references/customization-deep-dive.md +319 -518
- package/skills/purgetss/references/dynamic-component-creation.md +33 -37
- package/skills/purgetss/references/grid-layout.md +42 -371
- package/skills/purgetss/references/icon-fonts.md +82 -475
- package/skills/purgetss/references/installation-setup.md +159 -331
- package/skills/purgetss/references/migration-guide.md +54 -109
- package/skills/purgetss/references/opacity-modifier.md +25 -222
- package/skills/purgetss/references/performance-tips.md +2 -2
- package/skills/purgetss/references/platform-modifiers.md +21 -407
- package/skills/purgetss/references/tikit-components.md +237 -104
- package/skills/purgetss/references/titanium-resets.md +20 -21
- package/skills/purgetss/references/ui-ux-design.md +171 -1702
- package/skills/ti-api/SKILL.md +109 -0
- package/skills/ti-api/references/api-android.md +675 -0
- package/skills/ti-api/references/api-app-platform.md +636 -0
- package/skills/ti-api/references/api-core.md +764 -0
- package/skills/ti-api/references/api-data-network.md +641 -0
- package/skills/ti-api/references/api-media.md +655 -0
- package/skills/ti-api/references/api-modules-ble-bluetooth.md +657 -0
- package/skills/ti-api/references/api-modules-coremotion-urlsession.md +411 -0
- package/skills/ti-api/references/api-modules-map.md +632 -0
- package/skills/ti-api/references/api-modules-nfc.md +725 -0
- package/skills/ti-api/references/api-modules-social-misc.md +526 -0
- package/skills/ti-api/references/api-services.md +700 -0
- package/skills/ti-api/references/api-ui-android.md +499 -0
- package/skills/ti-api/references/api-ui-extras.md +702 -0
- package/skills/ti-api/references/api-ui-ios-animator.md +378 -0
- package/skills/ti-api/references/api-ui-ios.md +756 -0
- package/skills/ti-api/references/api-ui-lists.md +581 -0
- package/skills/ti-api/references/api-ui-text-input.md +607 -0
- package/skills/ti-api/references/api-ui-views.md +572 -0
- package/skills/ti-api/references/api-ui-windows-navigation.md +676 -0
- package/skills/ti-api/references/api-xml-global.md +743 -0
- package/skills/ti-expert/SKILL.md +106 -138
- package/skills/ti-expert/references/adaptive-layouts.md +414 -0
- package/skills/ti-expert/references/alloy-builtins.md +16 -20
- package/skills/ti-expert/references/alloy-structure.md +40 -42
- package/skills/ti-expert/references/anti-patterns.md +34 -0
- package/skills/ti-expert/references/architecture-tiers.md +248 -0
- package/skills/ti-expert/references/code-conventions.md +5 -3
- package/skills/ti-expert/references/error-handling.md +14 -7
- package/skills/ti-expert/references/examples.md +4 -2
- package/skills/ti-expert/references/performance-optimization.md +26 -24
- package/skills/ti-expert/references/security-device.md +17 -14
- package/skills/ti-expert/references/security-fundamentals.md +60 -101
- package/skills/ti-expert/references/state-management.md +15 -14
- package/skills/ti-expert/references/testing-e2e-ci.md +30 -21
- package/skills/ti-expert/references/theming.md +12 -20
- package/skills/ti-guides/SKILL.md +13 -17
- package/skills/ti-guides/references/advanced-data-and-images.md +30 -10
- package/skills/ti-guides/references/application-frameworks.md +3 -3
- package/skills/ti-guides/references/coding-best-practices.md +31 -2
- package/skills/ti-guides/references/commonjs-advanced.md +46 -4
- package/skills/ti-guides/references/hello-world.md +9 -13
- package/skills/ti-guides/references/hyperloop-native-access.md +4 -1
- package/skills/ti-guides/references/javascript-primer.md +13 -5
- package/skills/ti-guides/references/resources.md +0 -2
- package/skills/ti-guides/references/style-and-conventions.md +1 -0
- package/skills/ti-guides/references/tiapp-config.md +0 -32
- package/skills/ti-howtos/SKILL.md +43 -45
- package/skills/ti-howtos/references/buffer-codec-streams.md +25 -3
- package/skills/ti-howtos/references/debugging-profiling.md +14 -25
- package/skills/ti-howtos/references/google-maps-v2.md +3 -3
- package/skills/ti-howtos/references/ios-map-kit.md +8 -1
- package/skills/ti-howtos/references/notification-services.md +0 -1
- package/skills/ti-howtos/references/webpack-build-pipeline.md +3 -0
- package/skills/ti-ui/SKILL.md +47 -49
- package/skills/ti-ui/references/application-structures.md +3 -2
- package/skills/ti-ui/references/orientation.md +17 -9
- package/skills/ti-ui/references/platform-ui-ios.md +63 -0
- package/skills/ti-ui/references/scrolling-views.md +39 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
# Adaptive layouts
|
|
2
|
+
|
|
3
|
+
Patterns for building responsive Titanium apps that work across phones, tablets, foldables, and desktop mode.
|
|
4
|
+
|
|
5
|
+
This reference is NOT derived from official Titanium documentation. It addresses Google's Android 17 resizability requirements and provides reusable patterns for adaptive UI in Titanium/Alloy apps.
|
|
6
|
+
|
|
7
|
+
## 1. Context: Android 17 resizability requirements
|
|
8
|
+
|
|
9
|
+
Starting with Android 17 (API 37), on devices with screens >= 600dp (tablets, foldables, desktop mode), Google ignores these manifest attributes:
|
|
10
|
+
|
|
11
|
+
- `android:screenOrientation` (all values, including `portrait`)
|
|
12
|
+
- `setRequestedOrientation()`
|
|
13
|
+
- `android:resizeableActivity`
|
|
14
|
+
- `android:minAspectRatio` / `android:maxAspectRatio`
|
|
15
|
+
|
|
16
|
+
Deadline: August 2027 — Google Play requires `targetSdkVersion="37"` for all new apps and updates.
|
|
17
|
+
|
|
18
|
+
Scope: Only affects screens >= 600dp. Phones (< 600dp) and games (`android:appCategory="game"`) are exempt.
|
|
19
|
+
|
|
20
|
+
What this means: portrait-only apps will be forced into landscape/resized on large screens. If the UI doesn't support it, it will break.
|
|
21
|
+
|
|
22
|
+
### Related: edge-to-edge (API 36)
|
|
23
|
+
|
|
24
|
+
Before the resizability requirement (API 37), Android SDK 36 removes the opt-out for edge-to-edge rendering, causing Titanium layouts to overlap system navigation bars. The Titanium core team has a PR to handle this at the SDK level: tidev/titanium-sdk#14399 — adds an `EdgeToEdgeHelper` with correct padding so existing apps work without code changes.
|
|
25
|
+
|
|
26
|
+
| Android requirement | API level | Who handles it |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| Edge-to-edge (system bars) | SDK 36 | Titanium SDK (PR #14399) |
|
|
29
|
+
| Resizability / orientation unlock | SDK 37 | App developer (this reference) |
|
|
30
|
+
|
|
31
|
+
### Full requirements checklist
|
|
32
|
+
|
|
33
|
+
| Requirement | Ti.UI.FILL/SIZE solves it? | What to do in Titanium |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| No orientation lock on >= 600dp | Yes | Remove the lock from tiapp.xml |
|
|
36
|
+
| Multi-window / split-screen | Yes | Avoid fixed dimensions |
|
|
37
|
+
| Variable aspect ratios | Yes | Use FILL/SIZE/percentages |
|
|
38
|
+
| Preserve state on config changes | **Handled by Titanium** | Titanium declares `android:configChanges` on all Activities automatically — Android does NOT destroy/recreate them on rotation, resize, or split-screen. No extra code needed. |
|
|
39
|
+
| External input (keyboard, mouse) | **No** | Handle `keypressed` events, ensure touch targets work with mouse clicks |
|
|
40
|
+
|
|
41
|
+
For most apps, removing the portrait lock and using `Ti.UI.FILL`/`Ti.UI.SIZE` correctly is sufficient. External input is only needed for apps targeting desktop mode or connected displays.
|
|
42
|
+
|
|
43
|
+
### Two levels of adaptive layout
|
|
44
|
+
|
|
45
|
+
**Level 1 — Fluid layout (most apps):** Use `Ti.UI.FILL`, `Ti.UI.SIZE`, and percentages. No JS code needed. The layout engine adapts automatically. With PurgeTSS: `w-screen`, `h-auto`, and percentage utilities.
|
|
46
|
+
|
|
47
|
+
**Level 2 — Breakpoint-driven layout (structural changes):** Use the responsive helper + `postlayout` event + layout adapters when the structure must change (e.g., sidebar lateral on tablet, hidden on phone). The rest of this reference covers Level 2 patterns.
|
|
48
|
+
|
|
49
|
+
## 2. Detecting window size
|
|
50
|
+
|
|
51
|
+
### platformWidth vs win.size
|
|
52
|
+
|
|
53
|
+
There are two ways to get dimensions, and they behave differently:
|
|
54
|
+
|
|
55
|
+
| API | Returns | In split-screen |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `Ti.Platform.displayCaps.platformWidth` | Full device screen width (pixels on Android) | Always returns full device width — WRONG for layout decisions |
|
|
58
|
+
| `$.win.size.width` (after `postlayout`) | Actual window width in app's default unit | Returns the real available width — CORRECT for layout decisions |
|
|
59
|
+
|
|
60
|
+
Google explicitly recommends: "do not use device screen size for layout decisions — use the actual window size." In native Android this is `WindowManager.getCurrentWindowMetrics()`. In Titanium, the equivalent is `win.size` after `postlayout`.
|
|
61
|
+
|
|
62
|
+
### Units: dp is the default in Titanium
|
|
63
|
+
|
|
64
|
+
Titanium uses `dp` as the default unit. The `win.size` property returns values in the app's default unit. Since the default is dp, `win.size.width` already returns dp values — do NOT divide by `logicalDensityFactor` or you will get a double-conversion (halved values).
|
|
65
|
+
|
|
66
|
+
`platformWidth` is different: it returns absolute pixels on Android regardless of `ti.ui.defaultunit`. This requires dividing by `logicalDensityFactor` to convert to dp.
|
|
67
|
+
|
|
68
|
+
### Responsive helper — `app/lib/responsive.js`
|
|
69
|
+
|
|
70
|
+
```javascript
|
|
71
|
+
/**
|
|
72
|
+
* Detects breakpoints using the actual window size.
|
|
73
|
+
* Accurate in split-screen, multi-window, and freeform modes.
|
|
74
|
+
*
|
|
75
|
+
* win.size returns dp values (Titanium default unit is dp).
|
|
76
|
+
* Do NOT divide by logicalDensityFactor — that would double-convert.
|
|
77
|
+
*/
|
|
78
|
+
const getInfoFromWindow = (winSize) => {
|
|
79
|
+
const dpWidth = winSize.width
|
|
80
|
+
const dpHeight = winSize.height
|
|
81
|
+
|
|
82
|
+
const isPortrait = dpHeight > dpWidth
|
|
83
|
+
const isTablet = Math.min(dpWidth, dpHeight) >= 600
|
|
84
|
+
|
|
85
|
+
let breakpoint = 'phone'
|
|
86
|
+
if (dpWidth >= 900) {
|
|
87
|
+
breakpoint = 'desktop'
|
|
88
|
+
} else if (dpWidth >= 600) {
|
|
89
|
+
breakpoint = 'tablet'
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { dpWidth, dpHeight, isPortrait, isTablet, breakpoint }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Fallback: detects breakpoints using device screen size.
|
|
97
|
+
* Use only when win.size is not available (e.g., before postlayout).
|
|
98
|
+
* NOT accurate in split-screen — returns full device width.
|
|
99
|
+
*
|
|
100
|
+
* platformWidth returns pixels on Android — must convert to dp.
|
|
101
|
+
*/
|
|
102
|
+
const getInfoFromDevice = () => {
|
|
103
|
+
const { platformWidth: w, platformHeight: h, logicalDensityFactor: density } = Ti.Platform.displayCaps
|
|
104
|
+
|
|
105
|
+
const isPortrait = Ti.Gesture.portrait
|
|
106
|
+
const factor = OS_ANDROID ? density : 1
|
|
107
|
+
const dpWidth = (isPortrait ? Math.min(w, h) : Math.max(w, h)) / factor
|
|
108
|
+
const dpHeight = (isPortrait ? Math.max(w, h) : Math.min(w, h)) / factor
|
|
109
|
+
const isTablet = Math.min(w, h) / factor >= 600
|
|
110
|
+
|
|
111
|
+
let breakpoint = 'phone'
|
|
112
|
+
if (dpWidth >= 900) {
|
|
113
|
+
breakpoint = 'desktop'
|
|
114
|
+
} else if (dpWidth >= 600) {
|
|
115
|
+
breakpoint = 'tablet'
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { dpWidth, dpHeight, isPortrait, isTablet, breakpoint }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const onChange = (callback) => {
|
|
122
|
+
const handler = () => {
|
|
123
|
+
callback(getInfoFromDevice())
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
Ti.Gesture.addEventListener('orientationchange', handler)
|
|
127
|
+
return handler
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const removeOff = (handler) => {
|
|
131
|
+
Ti.Gesture.removeEventListener('orientationchange', handler)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = { getInfoFromWindow, getInfoFromDevice, onChange, removeOff }
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Quick inline detection
|
|
138
|
+
|
|
139
|
+
```javascript
|
|
140
|
+
// After postlayout — accurate in split-screen
|
|
141
|
+
const dpWidth = $.win.size.width // already dp, no conversion needed
|
|
142
|
+
const isTablet = Math.min($.win.size.width, $.win.size.height) >= 600
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## 3. Layout strategy
|
|
146
|
+
|
|
147
|
+
| Breakpoint | dpWidth | Strategy |
|
|
148
|
+
| ---------- | --------- | ------------------------------------- |
|
|
149
|
+
| Phone | < 600dp | Vertical stack, sidebar hidden |
|
|
150
|
+
| Tablet | 600-899dp | Side-by-side split (30/70 or 40/60) |
|
|
151
|
+
| Desktop | >= 900dp | Fixed or percentage sidebar + fluid content |
|
|
152
|
+
|
|
153
|
+
## 4. Controller pattern
|
|
154
|
+
|
|
155
|
+
### View — `app/views/example.xml`
|
|
156
|
+
|
|
157
|
+
```xml
|
|
158
|
+
<Alloy>
|
|
159
|
+
<Window id="win" backgroundColor="#fff">
|
|
160
|
+
<View id="container">
|
|
161
|
+
<View id="sidebar" />
|
|
162
|
+
<View id="content" />
|
|
163
|
+
</View>
|
|
164
|
+
</Window>
|
|
165
|
+
</Alloy>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Styles — `app/styles/example.tss`
|
|
169
|
+
|
|
170
|
+
```tss
|
|
171
|
+
"#container": {
|
|
172
|
+
width: Ti.UI.FILL,
|
|
173
|
+
height: Ti.UI.FILL
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
"#sidebar": {
|
|
177
|
+
backgroundColor: "#eee",
|
|
178
|
+
layout: "vertical"
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
"#content": {
|
|
182
|
+
backgroundColor: "#fff",
|
|
183
|
+
layout: "vertical"
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Controller — `app/controllers/example.js`
|
|
188
|
+
|
|
189
|
+
```javascript
|
|
190
|
+
const Responsive = require('/responsive')
|
|
191
|
+
|
|
192
|
+
let state = Responsive.getInfoFromDevice()
|
|
193
|
+
let postlayoutHandler
|
|
194
|
+
|
|
195
|
+
// --- Layout adapters ---
|
|
196
|
+
|
|
197
|
+
function layoutPhone() {
|
|
198
|
+
$.container.layout = 'vertical'
|
|
199
|
+
|
|
200
|
+
$.sidebar.visible = false
|
|
201
|
+
|
|
202
|
+
$.content.applyProperties({
|
|
203
|
+
height: Ti.UI.FILL,
|
|
204
|
+
width: Ti.UI.FILL
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function layoutTablet() {
|
|
209
|
+
$.container.layout = 'horizontal'
|
|
210
|
+
|
|
211
|
+
$.sidebar.applyProperties({
|
|
212
|
+
height: Ti.UI.FILL,
|
|
213
|
+
visible: true,
|
|
214
|
+
width: '30%'
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
$.content.applyProperties({
|
|
218
|
+
height: Ti.UI.FILL,
|
|
219
|
+
width: '70%'
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function layoutDesktop() {
|
|
224
|
+
$.container.layout = 'horizontal'
|
|
225
|
+
|
|
226
|
+
$.sidebar.applyProperties({
|
|
227
|
+
height: Ti.UI.FILL,
|
|
228
|
+
visible: true,
|
|
229
|
+
width: 280
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
$.content.applyProperties({
|
|
233
|
+
height: Ti.UI.FILL,
|
|
234
|
+
width: Ti.UI.FILL
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// --- Apply layout based on breakpoint ---
|
|
239
|
+
|
|
240
|
+
function applyLayout() {
|
|
241
|
+
switch (state.breakpoint) {
|
|
242
|
+
case 'phone':
|
|
243
|
+
layoutPhone()
|
|
244
|
+
break
|
|
245
|
+
case 'tablet':
|
|
246
|
+
layoutTablet()
|
|
247
|
+
break
|
|
248
|
+
case 'desktop':
|
|
249
|
+
layoutDesktop()
|
|
250
|
+
break
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// --- Lifecycle ---
|
|
255
|
+
|
|
256
|
+
// Use postlayout to detect actual window size.
|
|
257
|
+
// This is accurate in split-screen where platformWidth is not.
|
|
258
|
+
postlayoutHandler = () => {
|
|
259
|
+
const newState = Responsive.getInfoFromWindow($.win.size)
|
|
260
|
+
const changed = newState.breakpoint !== state.breakpoint
|
|
261
|
+
|
|
262
|
+
state = newState
|
|
263
|
+
|
|
264
|
+
// Only re-apply layout when breakpoint changes.
|
|
265
|
+
// Percentage-based widths adapt automatically via the layout engine.
|
|
266
|
+
// Re-applying the same layout causes visible double-adjustment flicker.
|
|
267
|
+
if (changed) {
|
|
268
|
+
applyLayout()
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
$.win.addEventListener('postlayout', postlayoutHandler)
|
|
273
|
+
|
|
274
|
+
applyLayout()
|
|
275
|
+
|
|
276
|
+
// --- Memory cleanup (mandatory for controllers with global listeners) ---
|
|
277
|
+
|
|
278
|
+
function cleanup() {
|
|
279
|
+
if (postlayoutHandler) {
|
|
280
|
+
$.win.removeEventListener('postlayout', postlayoutHandler)
|
|
281
|
+
postlayoutHandler = null
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
$.win.addEventListener('close', cleanup)
|
|
286
|
+
$.cleanup = cleanup
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## 5. Grid simulation
|
|
290
|
+
|
|
291
|
+
Titanium does NOT have a `gap` property. Use `left` / `top` offsets in horizontal/vertical layouts to simulate spacing between siblings.
|
|
292
|
+
|
|
293
|
+
```javascript
|
|
294
|
+
function createRow() {
|
|
295
|
+
return Ti.UI.createView({
|
|
296
|
+
layout: 'horizontal',
|
|
297
|
+
height: Ti.UI.SIZE,
|
|
298
|
+
width: Ti.UI.FILL
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function createCol(percent, spacing) {
|
|
303
|
+
return Ti.UI.createView({
|
|
304
|
+
width: percent + '%',
|
|
305
|
+
height: Ti.UI.SIZE,
|
|
306
|
+
left: spacing || 0
|
|
307
|
+
})
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Usage in a tablet layout:
|
|
311
|
+
function layoutTabletGrid() {
|
|
312
|
+
$.content.removeAllChildren()
|
|
313
|
+
|
|
314
|
+
const row = createRow()
|
|
315
|
+
|
|
316
|
+
const col1 = createCol(48)
|
|
317
|
+
col1.add(Ti.UI.createLabel({ text: 'Left column' }))
|
|
318
|
+
|
|
319
|
+
// 4% gap simulated via left offset
|
|
320
|
+
const col2 = createCol(48, '4%')
|
|
321
|
+
col2.add(Ti.UI.createLabel({ text: 'Right column' }))
|
|
322
|
+
|
|
323
|
+
row.add(col1)
|
|
324
|
+
row.add(col2)
|
|
325
|
+
|
|
326
|
+
$.content.add(row)
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## 6. Anti-patterns
|
|
331
|
+
|
|
332
|
+
```javascript
|
|
333
|
+
// ❌ Using platformWidth for layout decisions in split-screen
|
|
334
|
+
// Returns full device width, not the app's window width
|
|
335
|
+
const dpWidth = Ti.Platform.displayCaps.platformWidth / density
|
|
336
|
+
// Use $.win.size.width after postlayout instead
|
|
337
|
+
|
|
338
|
+
// ❌ Dividing win.size by logicalDensityFactor
|
|
339
|
+
// win.size already returns dp (Titanium's default unit) — double-conversion!
|
|
340
|
+
const dpWidth = $.win.size.width / density // WRONG: halves the value
|
|
341
|
+
|
|
342
|
+
// ❌ Using Ti.Gesture.isPortrait() — not a method, it's a property
|
|
343
|
+
Ti.Gesture.isPortrait() // TypeError!
|
|
344
|
+
Ti.Gesture.portrait // correct — boolean property
|
|
345
|
+
|
|
346
|
+
// ❌ Using gap property — does NOT exist in Titanium
|
|
347
|
+
{ layout: 'horizontal', gap: 10 }
|
|
348
|
+
|
|
349
|
+
// ❌ Anonymous listeners on global objects — memory leak, can never be removed
|
|
350
|
+
Ti.Gesture.addEventListener('orientationchange', () => { ... })
|
|
351
|
+
|
|
352
|
+
// ❌ Re-applying layout when breakpoint hasn't changed — causes flicker
|
|
353
|
+
// Percentage-based widths already adapt via the layout engine
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
## 7. tiapp.xml configuration
|
|
357
|
+
|
|
358
|
+
### Android: enable multiple orientations
|
|
359
|
+
|
|
360
|
+
The `<activity>` element must include `android:name` and be nested inside `<application>`. Without `android:name`, Android's manifest merger fails with "Missing 'name' key attribute on element activity".
|
|
361
|
+
|
|
362
|
+
Titanium generates the main activity name as `<app-id>.<Appname>Activity`. For example, app ID `com.example.myapp` with name `MyApp` produces `com.example.myapp.MyappActivity`.
|
|
363
|
+
|
|
364
|
+
```xml
|
|
365
|
+
<android xmlns:android="http://schemas.android.com/apk/res/android">
|
|
366
|
+
<manifest>
|
|
367
|
+
<application>
|
|
368
|
+
<activity
|
|
369
|
+
android:name="com.example.myapp.MyappActivity"
|
|
370
|
+
android:screenOrientation="fullSensor"/>
|
|
371
|
+
</application>
|
|
372
|
+
</manifest>
|
|
373
|
+
</android>
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
To verify the exact generated activity name, check `build/android/AndroidManifest.xml` after a build.
|
|
377
|
+
|
|
378
|
+
### iOS: support all orientations on iPad
|
|
379
|
+
|
|
380
|
+
```xml
|
|
381
|
+
<ios>
|
|
382
|
+
<plist>
|
|
383
|
+
<dict>
|
|
384
|
+
<key>UISupportedInterfaceOrientations</key>
|
|
385
|
+
<array>
|
|
386
|
+
<string>UIInterfaceOrientationPortrait</string>
|
|
387
|
+
</array>
|
|
388
|
+
<key>UISupportedInterfaceOrientations~ipad</key>
|
|
389
|
+
<array>
|
|
390
|
+
<string>UIInterfaceOrientationPortrait</string>
|
|
391
|
+
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
|
392
|
+
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
393
|
+
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
394
|
+
</array>
|
|
395
|
+
</dict>
|
|
396
|
+
</plist>
|
|
397
|
+
</ios>
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
## 8. Quick reference
|
|
401
|
+
|
|
402
|
+
| Concept | Titanium API |
|
|
403
|
+
| --- | --- |
|
|
404
|
+
| Window size (split-screen safe) | `$.win.size.width` / `$.win.size.height` after `postlayout` — already in dp |
|
|
405
|
+
| Device screen size | `Ti.Platform.displayCaps.platformWidth` — pixels on Android, needs `/logicalDensityFactor` |
|
|
406
|
+
| Orientation (boolean) | `Ti.Gesture.portrait`, `Ti.Gesture.landscape` — properties, not methods |
|
|
407
|
+
| Orientation event | `Ti.Gesture.addEventListener('orientationchange', handler)` |
|
|
408
|
+
| Window resize event | `$.win.addEventListener('postlayout', handler)` — fires on rotation, split-screen, resize |
|
|
409
|
+
| Auto-size | `Ti.UI.SIZE` (fit content), `Ti.UI.FILL` (fill remaining) |
|
|
410
|
+
| Percentages | `width: '50%'` relative to parent |
|
|
411
|
+
| Spacing | `left`/`top` as offset from previous sibling in horizontal/vertical |
|
|
412
|
+
| Batch updates | `view.applyProperties({...})` to reduce bridge overhead |
|
|
413
|
+
|
|
414
|
+
For detailed layout APIs, see the `ti-ui` skill's [layouts-and-positioning.md](../../ti-ui/references/layouts-and-positioning.md) and [orientation.md](../../ti-ui/references/orientation.md).
|
|
@@ -23,12 +23,11 @@ if (OS_ANDROID) {
|
|
|
23
23
|
}
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
`OS_IOS` and `OS_ANDROID` are replaced by the Alloy compiler with literal `true`/`false`. The opposite platform's code is completely removed from the build output. This is different from `Ti.Platform.osname === 'iphone'` which remains in the code.
|
|
28
|
-
|
|
29
|
-
**Use for:** Platform-specific APIs, platform-specific UI configuration
|
|
30
|
-
**Do NOT use for:** Logic that should be testable on both platforms
|
|
31
|
-
:::
|
|
26
|
+
> **⚠️ These are NOT runtime checks**
|
|
27
|
+
> `OS_IOS` and `OS_ANDROID` are replaced by the Alloy compiler with literal `true`/`false`. The opposite platform's code is completely removed from the build output. This is different from `Ti.Platform.osname === 'iphone'` which remains in the code.
|
|
28
|
+
>
|
|
29
|
+
> **Use for:** Platform-specific APIs, platform-specific UI configuration
|
|
30
|
+
> **Do NOT use for:** Logic that should be testable on both platforms
|
|
32
31
|
|
|
33
32
|
### Env_dev / env_test / env_production
|
|
34
33
|
|
|
@@ -132,9 +131,8 @@ const debug = Alloy.CFG.debug // true in dev, false in prod
|
|
|
132
131
|
const appName = Alloy.CFG.appName // From global
|
|
133
132
|
```
|
|
134
133
|
|
|
135
|
-
|
|
136
|
-
`Alloy.CFG` values are set at build time and cannot be modified at runtime. For runtime configuration, use `Alloy.Globals` or `Ti.App.Properties`.
|
|
137
|
-
:::
|
|
134
|
+
> **💡 CFG is read-only**
|
|
135
|
+
> `Alloy.CFG` values are set at build time and cannot be modified at runtime. For runtime configuration, use `Alloy.Globals` or `Ti.App.Properties`.
|
|
138
136
|
|
|
139
137
|
---
|
|
140
138
|
|
|
@@ -256,14 +254,13 @@ const title = $.args.title || ''
|
|
|
256
254
|
const showBack = $.args.showBack !== 'false' // XML passes strings
|
|
257
255
|
```
|
|
258
256
|
|
|
259
|
-
|
|
260
|
-
When passing values through XML attributes, they arrive as strings in `$.args`. Numbers and booleans need to be parsed:
|
|
261
|
-
```javascript
|
|
262
|
-
const count = parseInt($.args.count, 10) // "5" → 5
|
|
263
|
-
const visible = $.args.visible !== 'false' // "false" → false
|
|
264
|
-
```
|
|
265
|
-
For complex data, pass via `Alloy.createController()` in the parent controller instead.
|
|
266
|
-
:::
|
|
257
|
+
> **⚠️ XML attributes are always strings**
|
|
258
|
+
> When passing values through XML attributes, they arrive as strings in `$.args`. Numbers and booleans need to be parsed:
|
|
259
|
+
> ```javascript
|
|
260
|
+
> const count = parseInt($.args.count, 10) // "5" → 5
|
|
261
|
+
> const visible = $.args.visible !== 'false' // "false" → false
|
|
262
|
+
> ```
|
|
263
|
+
> For complex data, pass via `Alloy.createController()` in the parent controller instead.
|
|
267
264
|
|
|
268
265
|
---
|
|
269
266
|
|
|
@@ -365,9 +362,8 @@ users.fetch({
|
|
|
365
362
|
</View>
|
|
366
363
|
```
|
|
367
364
|
|
|
368
|
-
|
|
369
|
-
The `if` attribute is checked when the controller is created. It does NOT react to later changes in the condition. For dynamic visibility, use the `visible` property in the controller.
|
|
370
|
-
:::
|
|
365
|
+
> **⚠️ `if` is evaluated once at controller creation**
|
|
366
|
+
> The `if` attribute is checked when the controller is created. It does NOT react to later changes in the condition. For dynamic visibility, use the `visible` property in the controller.
|
|
371
367
|
|
|
372
368
|
---
|
|
373
369
|
|
|
@@ -66,45 +66,44 @@ app/
|
|
|
66
66
|
|
|
67
67
|
## lib/ folder and module require paths
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
When Alloy compiles, the **entire `lib/` folder is flattened to the root of Resources**. This means:
|
|
71
|
-
- `app/lib/services/authService.js` → `Resources/iphone/services/authService.js`
|
|
72
|
-
- `app/lib/api/authApi.js` → `Resources/iphone/api/authApi.js`
|
|
73
|
-
|
|
74
|
-
**Therefore, require statements should NOT include `lib/` prefix:**
|
|
75
|
-
```javascript
|
|
76
|
-
// ❌ WRONG - Will fail at runtime
|
|
77
|
-
const authApi = require('lib/api/authApi')
|
|
78
|
-
|
|
79
|
-
// ✅ CORRECT - Path relative to flattened lib/
|
|
80
|
-
const authApi = require('api/authApi')
|
|
81
|
-
const authService = require('services/authService')
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
**This applies to:**
|
|
85
|
-
- All files in `app/lib/` (services, api, helpers, etc.)
|
|
86
|
-
- Cross-references within lib/ files
|
|
87
|
-
- Controller requires of lib/ files
|
|
88
|
-
|
|
89
|
-
**Example project structure:**
|
|
90
|
-
```
|
|
91
|
-
app/
|
|
92
|
-
├── lib/
|
|
93
|
-
│ ├── services/
|
|
94
|
-
│ │ ├── authService.js # require('services/navigationService')
|
|
95
|
-
│ │ ├── navigationService.js # require('services/notificationService')
|
|
96
|
-
│ │ └── notificationService.js
|
|
97
|
-
│ ├── api/
|
|
98
|
-
│ │ ├── authApi.js # require('services/authService')
|
|
99
|
-
│ │ ├── userApi.js
|
|
100
|
-
│ │ └── frameApi.js
|
|
101
|
-
│ └── repositories/
|
|
102
|
-
│ ├── userRepository.js
|
|
103
|
-
│ └── settingsRepository.js
|
|
104
|
-
├── controllers/
|
|
105
|
-
│ └── index.js # require('services/authService')
|
|
106
|
-
```
|
|
107
|
-
:::
|
|
69
|
+
> **🚨 CRITICAL: lib/ Folder is FLATTENED During Build**
|
|
70
|
+
> When Alloy compiles, the **entire `lib/` folder is flattened to the root of Resources**. This means:
|
|
71
|
+
> - `app/lib/services/authService.js` → `Resources/iphone/services/authService.js`
|
|
72
|
+
> - `app/lib/api/authApi.js` → `Resources/iphone/api/authApi.js`
|
|
73
|
+
>
|
|
74
|
+
> **Therefore, require statements should NOT include `lib/` prefix:**
|
|
75
|
+
> ```javascript
|
|
76
|
+
> // ❌ WRONG - Will fail at runtime
|
|
77
|
+
> const authApi = require('lib/api/authApi')
|
|
78
|
+
>
|
|
79
|
+
> // ✅ CORRECT - Path relative to flattened lib/
|
|
80
|
+
> const authApi = require('api/authApi')
|
|
81
|
+
> const authService = require('services/authService')
|
|
82
|
+
> ```
|
|
83
|
+
>
|
|
84
|
+
> **This applies to:**
|
|
85
|
+
> - All files in `app/lib/` (services, api, helpers, etc.)
|
|
86
|
+
> - Cross-references within lib/ files
|
|
87
|
+
> - Controller requires of lib/ files
|
|
88
|
+
>
|
|
89
|
+
> **Example project structure:**
|
|
90
|
+
> ```
|
|
91
|
+
> app/
|
|
92
|
+
> ├── lib/
|
|
93
|
+
> │ ├── services/
|
|
94
|
+
> │ │ ├── authService.js # require('services/navigationService')
|
|
95
|
+
> │ │ ├── navigationService.js # require('services/notificationService')
|
|
96
|
+
> │ │ └── notificationService.js
|
|
97
|
+
> │ ├── api/
|
|
98
|
+
> │ │ ├── authApi.js # require('services/authService')
|
|
99
|
+
> │ │ ├── userApi.js
|
|
100
|
+
> │ │ └── frameApi.js
|
|
101
|
+
> │ └── repositories/
|
|
102
|
+
> │ ├── userRepository.js
|
|
103
|
+
> │ └── settingsRepository.js
|
|
104
|
+
> ├── controllers/
|
|
105
|
+
> │ └── index.js # require('services/authService')
|
|
106
|
+
> ```
|
|
108
107
|
|
|
109
108
|
## Data layer: two approaches
|
|
110
109
|
|
|
@@ -338,9 +337,8 @@ const loadData = () => {
|
|
|
338
337
|
}
|
|
339
338
|
```
|
|
340
339
|
|
|
341
|
-
|
|
342
|
-
Widgets have their own `styles/widget.tss` file. Define all widget-specific styles there to keep them self-contained and portable.
|
|
343
|
-
:::
|
|
340
|
+
> **💡 Widget Styles**
|
|
341
|
+
> Widgets have their own `styles/widget.tss` file. Define all widget-specific styles there to keep them self-contained and portable.
|
|
344
342
|
|
|
345
343
|
### Widget ↔ controller communication
|
|
346
344
|
|
|
@@ -76,6 +76,39 @@
|
|
|
76
76
|
- iOS: Use `Ti.UI.iOS.createDocumentViewer` for files, or simple `Ti.UI.createOptionDialog` + `Ti.UI.Clipboard` for links
|
|
77
77
|
- Android: Use `Ti.Android.createIntent` with ACTION_SEND
|
|
78
78
|
|
|
79
|
+
## Community-Discovered Patterns
|
|
80
|
+
|
|
81
|
+
### 15. Using extendEdges without autoAdjustScrollViewInsets (iOS)
|
|
82
|
+
|
|
83
|
+
**Anti-pattern:** Setting `extendEdges: [Ti.UI.EXTEND_EDGE_ALL]` on a Window without also setting `autoAdjustScrollViewInsets: true`.
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
// Wrong: content overlaps behind navigation bar
|
|
87
|
+
const win = Ti.UI.createWindow({
|
|
88
|
+
title: 'My Screen',
|
|
89
|
+
largeTitleEnabled: true,
|
|
90
|
+
extendEdges: [Ti.UI.EXTEND_EDGE_ALL]
|
|
91
|
+
// missing autoAdjustScrollViewInsets!
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Why it's wrong:** `extendEdges` tells iOS to extend the view's content behind the navigation bar and tab bar. Without `autoAdjustScrollViewInsets: true`, iOS does not adjust the ScrollView's content insets, so the scroll content starts at y=0 — directly behind the navigation bar.
|
|
96
|
+
|
|
97
|
+
**Fix:** Always pair `extendEdges` with `autoAdjustScrollViewInsets`:
|
|
98
|
+
|
|
99
|
+
```javascript
|
|
100
|
+
const win = Ti.UI.createWindow({
|
|
101
|
+
title: 'My Screen',
|
|
102
|
+
largeTitleEnabled: true,
|
|
103
|
+
extendEdges: [Ti.UI.EXTEND_EDGE_ALL],
|
|
104
|
+
autoAdjustScrollViewInsets: true
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
These three properties work together: `extendEdges` creates the blur/translucent effect, `autoAdjustScrollViewInsets` prevents content overlap, and `largeTitleEnabled` shows the collapsible large title. Without `extendEdges`, the large title also renders with a visible delay (empty nav bar gap appears first, then the title draws).
|
|
109
|
+
|
|
110
|
+
This applies to Windows inside both standalone NavigationWindow and TabGroup (which wraps each Tab in an implicit NavigationWindow on iOS).
|
|
111
|
+
|
|
79
112
|
---
|
|
80
113
|
|
|
81
114
|
## Quick reference table
|
|
@@ -88,3 +121,4 @@
|
|
|
88
121
|
| `lib/` prefix | lib/ is flattened | Use path without lib/ |
|
|
89
122
|
| `$.index.open()` | Wrong ID reference | Use actual Window ID |
|
|
90
123
|
| `createNotification` | API doesn't exist | `createAlertDialog` |
|
|
124
|
+
| `extendEdges` alone | Content behind nav bar | Add `autoAdjustScrollViewInsets: true` |
|