@chancestv/tv-focus 0.2.0 → 0.4.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/dist/index.js +211 -256
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/engine/ATTRIBUTION.md +11 -1
- package/src/engine/spatial-navigation.ts +210 -257
|
@@ -112,300 +112,196 @@ var $: any = null;
|
|
|
112
112
|
return rect;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (center.y < targetRect.top) {
|
|
132
|
-
y = 0;
|
|
133
|
-
} else if (center.y <= targetRect.bottom) {
|
|
134
|
-
y = 1;
|
|
135
|
-
} else {
|
|
136
|
-
y = 2;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
groupId = y * 3 + x;
|
|
140
|
-
groups[groupId].push(rect);
|
|
141
|
-
|
|
142
|
-
if ([0, 2, 6, 8].indexOf(groupId) !== -1) {
|
|
143
|
-
var threshold = straightOverlapThreshold;
|
|
144
|
-
|
|
145
|
-
if (rect.left <= targetRect.right - targetRect.width * threshold) {
|
|
146
|
-
if (groupId === 2) {
|
|
147
|
-
groups[1].push(rect);
|
|
148
|
-
} else if (groupId === 8) {
|
|
149
|
-
groups[7].push(rect);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (rect.right >= targetRect.left + targetRect.width * threshold) {
|
|
154
|
-
if (groupId === 0) {
|
|
155
|
-
groups[1].push(rect);
|
|
156
|
-
} else if (groupId === 6) {
|
|
157
|
-
groups[7].push(rect);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (rect.top <= targetRect.bottom - targetRect.height * threshold) {
|
|
162
|
-
if (groupId === 6) {
|
|
163
|
-
groups[3].push(rect);
|
|
164
|
-
} else if (groupId === 8) {
|
|
165
|
-
groups[5].push(rect);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (rect.bottom >= targetRect.top + targetRect.height * threshold) {
|
|
170
|
-
if (groupId === 0) {
|
|
171
|
-
groups[3].push(rect);
|
|
172
|
-
} else if (groupId === 2) {
|
|
173
|
-
groups[5].push(rect);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
115
|
+
/*****************************************/
|
|
116
|
+
/* Android FocusFinder 加权距离模型(ES5)*/
|
|
117
|
+
/*****************************************/
|
|
118
|
+
// 移植自 android.view.FocusFinder:候选先过 isCandidate 方向门,
|
|
119
|
+
// 再两两 isBetterCandidate 比较(beam 内优先 + 13*major²+minor² 加权距离)。
|
|
120
|
+
// direction 用字符串 'left'|'right'|'up'|'down';rect 用 getRect 结构(含 center)。
|
|
121
|
+
|
|
122
|
+
function snMajorAxisDistanceRaw(direction, source, dest) {
|
|
123
|
+
switch (direction) {
|
|
124
|
+
case 'left': return source.left - dest.right;
|
|
125
|
+
case 'right': return dest.left - source.right;
|
|
126
|
+
case 'up': return source.top - dest.bottom;
|
|
127
|
+
case 'down': return dest.top - source.bottom;
|
|
128
|
+
default: return 0;
|
|
177
129
|
}
|
|
178
|
-
|
|
179
|
-
return groups;
|
|
180
130
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
nearTargetTopIsBetter: function(rect) {
|
|
212
|
-
var d;
|
|
213
|
-
if (rect.center.y < targetRect.center.y) {
|
|
214
|
-
d = targetRect.top - rect.bottom;
|
|
215
|
-
} else {
|
|
216
|
-
d = rect.top - targetRect.top;
|
|
217
|
-
}
|
|
218
|
-
return d < 0 ? 0 : d;
|
|
219
|
-
},
|
|
220
|
-
topIsBetter: function(rect) {
|
|
221
|
-
return rect.top;
|
|
222
|
-
},
|
|
223
|
-
bottomIsBetter: function(rect) {
|
|
224
|
-
return -1 * rect.bottom;
|
|
225
|
-
},
|
|
226
|
-
leftIsBetter: function(rect) {
|
|
227
|
-
return rect.left;
|
|
228
|
-
},
|
|
229
|
-
rightIsBetter: function(rect) {
|
|
230
|
-
return -1 * rect.right;
|
|
231
|
-
}
|
|
232
|
-
};
|
|
131
|
+
function snMajorAxisDistance(direction, source, dest) {
|
|
132
|
+
return Math.max(0, snMajorAxisDistanceRaw(direction, source, dest));
|
|
133
|
+
}
|
|
134
|
+
function snMajorAxisDistanceToFarEdgeRaw(direction, source, dest) {
|
|
135
|
+
switch (direction) {
|
|
136
|
+
case 'left': return source.left - dest.left;
|
|
137
|
+
case 'right': return dest.right - source.right;
|
|
138
|
+
case 'up': return source.top - dest.top;
|
|
139
|
+
case 'down': return dest.bottom - source.bottom;
|
|
140
|
+
default: return 0;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function snMajorAxisDistanceToFarEdge(direction, source, dest) {
|
|
144
|
+
return Math.max(1, snMajorAxisDistanceToFarEdgeRaw(direction, source, dest));
|
|
145
|
+
}
|
|
146
|
+
function snMinorAxisDistance(direction, source, dest) {
|
|
147
|
+
// 引擎 center.x/y 已是中心点,等价 Android 的 top+height/2 / left+width/2
|
|
148
|
+
switch (direction) {
|
|
149
|
+
case 'left':
|
|
150
|
+
case 'right':
|
|
151
|
+
return Math.abs(source.center.y - dest.center.y); // 垂直中心偏移
|
|
152
|
+
case 'up':
|
|
153
|
+
case 'down':
|
|
154
|
+
return Math.abs(source.center.x - dest.center.x); // 水平中心偏移
|
|
155
|
+
default:
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function snGetWeightedDistanceFor(major, minor) {
|
|
160
|
+
return 13 * major * major + minor * minor;
|
|
233
161
|
}
|
|
234
162
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
163
|
+
// dest 是否完全在 source 的 direction 方向(边界版,Android isToDirectionOf)
|
|
164
|
+
function snIsToDirectionOf(direction, source, dest) {
|
|
165
|
+
switch (direction) {
|
|
166
|
+
case 'left': return source.left >= dest.right;
|
|
167
|
+
case 'right': return source.right <= dest.left;
|
|
168
|
+
case 'up': return source.top >= dest.bottom;
|
|
169
|
+
case 'down': return source.bottom <= dest.top;
|
|
170
|
+
default: return false;
|
|
242
171
|
}
|
|
172
|
+
}
|
|
243
173
|
|
|
244
|
-
|
|
245
|
-
|
|
174
|
+
// dest 是否至少部分在 source 的 direction 方向(Android isCandidate)
|
|
175
|
+
function snIsCandidate(source, dest, direction) {
|
|
176
|
+
switch (direction) {
|
|
177
|
+
case 'left':
|
|
178
|
+
return (source.right > dest.right || source.left >= dest.right) &&
|
|
179
|
+
source.left > dest.left;
|
|
180
|
+
case 'right':
|
|
181
|
+
return (source.left < dest.left || source.right <= dest.left) &&
|
|
182
|
+
source.right < dest.right;
|
|
183
|
+
case 'up':
|
|
184
|
+
return (source.bottom > dest.bottom || source.top >= dest.bottom) &&
|
|
185
|
+
source.top > dest.top;
|
|
186
|
+
case 'down':
|
|
187
|
+
return (source.top < dest.top || source.bottom <= dest.top) &&
|
|
188
|
+
source.bottom < dest.bottom;
|
|
189
|
+
default:
|
|
190
|
+
return false;
|
|
246
191
|
}
|
|
192
|
+
}
|
|
247
193
|
|
|
248
|
-
|
|
194
|
+
// 次轴投影是否重叠(Android beamsOverlap)
|
|
195
|
+
function snBeamsOverlap(direction, rect1, rect2) {
|
|
196
|
+
switch (direction) {
|
|
197
|
+
case 'left':
|
|
198
|
+
case 'right':
|
|
199
|
+
return (rect2.bottom >= rect1.top) && (rect2.top <= rect1.bottom);
|
|
200
|
+
case 'up':
|
|
201
|
+
case 'down':
|
|
202
|
+
return (rect2.right >= rect1.left) && (rect2.left <= rect1.right);
|
|
203
|
+
default:
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
249
207
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
return
|
|
259
|
-
}
|
|
208
|
+
// rect1 是否凭「beam 内」胜出 rect2(Android beamBeats)
|
|
209
|
+
function snBeamBeats(direction, source, rect1, rect2) {
|
|
210
|
+
var r1In = snBeamsOverlap(direction, source, rect1);
|
|
211
|
+
var r2In = snBeamsOverlap(direction, source, rect2);
|
|
212
|
+
if (r2In || !r1In) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
if (!snIsToDirectionOf(direction, source, rect2)) {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
if (direction === 'left' || direction === 'right') {
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
return snMajorAxisDistance(direction, source, rect1) <
|
|
222
|
+
snMajorAxisDistanceToFarEdge(direction, source, rect2);
|
|
223
|
+
}
|
|
260
224
|
|
|
261
|
-
|
|
225
|
+
// rect1 是否比 rect2 更优(Android isBetterCandidate)
|
|
226
|
+
function snIsBetterCandidate(direction, source, rect1, rect2) {
|
|
227
|
+
if (!snIsCandidate(source, rect1, direction)) {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
if (!snIsCandidate(source, rect2, direction)) {
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
if (snBeamBeats(direction, source, rect1, rect2)) {
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
if (snBeamBeats(direction, source, rect2, rect1)) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
return snGetWeightedDistanceFor(
|
|
240
|
+
snMajorAxisDistance(direction, source, rect1),
|
|
241
|
+
snMinorAxisDistance(direction, source, rect1)) <
|
|
242
|
+
snGetWeightedDistanceFor(
|
|
243
|
+
snMajorAxisDistance(direction, source, rect2),
|
|
244
|
+
snMinorAxisDistance(direction, source, rect2));
|
|
262
245
|
}
|
|
263
246
|
|
|
264
|
-
// preferNearest
|
|
265
|
-
//
|
|
247
|
+
// preferNearest 入参保留以兼容现有调用(navigateWithinScrollScope 仍传 true),
|
|
248
|
+
// Android 模型下不再控制打分分层,仅占位。
|
|
266
249
|
function navigate(target, direction, candidates, config, preferNearest) {
|
|
267
250
|
if (!target || !direction || !candidates || !candidates.length) {
|
|
268
251
|
return null;
|
|
269
252
|
}
|
|
253
|
+
var targetRect = getRect(target);
|
|
254
|
+
if (!targetRect) {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
270
257
|
|
|
258
|
+
// 1) 转 rect + 过 isCandidate 方向门(straightOnly 时额外要求 beam 内)
|
|
271
259
|
var rects = [];
|
|
272
260
|
for (var i = 0; i < candidates.length; i++) {
|
|
273
|
-
var
|
|
274
|
-
if (
|
|
275
|
-
|
|
261
|
+
var r = getRect(candidates[i]);
|
|
262
|
+
if (!r) {
|
|
263
|
+
continue;
|
|
276
264
|
}
|
|
265
|
+
if (!snIsCandidate(targetRect, r, direction)) {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (config.straightOnly && !snBeamsOverlap(direction, targetRect, r)) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
rects.push(r);
|
|
277
272
|
}
|
|
278
273
|
if (!rects.length) {
|
|
279
274
|
return null;
|
|
280
275
|
}
|
|
281
276
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
277
|
+
// 2) 两两 isBetterCandidate 维护当前最优
|
|
278
|
+
var best = rects[0];
|
|
279
|
+
for (var k = 1; k < rects.length; k++) {
|
|
280
|
+
if (snIsBetterCandidate(direction, targetRect, rects[k], best)) {
|
|
281
|
+
best = rects[k];
|
|
282
|
+
}
|
|
285
283
|
}
|
|
286
284
|
|
|
287
|
-
var distanceFunction = generateDistanceFunction(targetRect);
|
|
288
|
-
|
|
289
|
-
var groups = partition(
|
|
290
|
-
rects,
|
|
291
|
-
targetRect,
|
|
292
|
-
config.straightOverlapThreshold
|
|
293
|
-
);
|
|
294
285
|
|
|
295
|
-
var internalGroups = partition(
|
|
296
|
-
groups[4],
|
|
297
|
-
targetRect.center,
|
|
298
|
-
config.straightOverlapThreshold
|
|
299
|
-
);
|
|
300
286
|
|
|
301
|
-
var priorities;
|
|
302
|
-
var df = distanceFunction;
|
|
303
|
-
var internalGroup, straightGroup, diagonalGroups;
|
|
304
|
-
var internalDist, straightDist, diagonalDist, mergedDist;
|
|
305
287
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
internalGroup = internalGroups[0].concat(internalGroups[3])
|
|
309
|
-
.concat(internalGroups[6]);
|
|
310
|
-
straightGroup = groups[3];
|
|
311
|
-
diagonalGroups = groups[0].concat(groups[6]);
|
|
312
|
-
internalDist = [df.nearPlumbLineIsBetter, df.topIsBetter];
|
|
313
|
-
straightDist = [df.nearPlumbLineIsBetter, df.topIsBetter];
|
|
314
|
-
diagonalDist = [df.nearHorizonIsBetter, df.rightIsBetter,
|
|
315
|
-
df.nearTargetTopIsBetter];
|
|
316
|
-
// 合并时主轴=水平最近,次轴=垂直对齐
|
|
317
|
-
mergedDist = [df.nearPlumbLineIsBetter, df.nearHorizonIsBetter,
|
|
318
|
-
df.topIsBetter];
|
|
319
|
-
break;
|
|
320
|
-
case 'right':
|
|
321
|
-
internalGroup = internalGroups[2].concat(internalGroups[5])
|
|
322
|
-
.concat(internalGroups[8]);
|
|
323
|
-
straightGroup = groups[5];
|
|
324
|
-
diagonalGroups = groups[2].concat(groups[8]);
|
|
325
|
-
internalDist = [df.nearPlumbLineIsBetter, df.topIsBetter];
|
|
326
|
-
straightDist = [df.nearPlumbLineIsBetter, df.topIsBetter];
|
|
327
|
-
diagonalDist = [df.nearHorizonIsBetter, df.leftIsBetter,
|
|
328
|
-
df.nearTargetTopIsBetter];
|
|
329
|
-
mergedDist = [df.nearPlumbLineIsBetter, df.nearHorizonIsBetter,
|
|
330
|
-
df.topIsBetter];
|
|
331
|
-
break;
|
|
332
|
-
case 'up':
|
|
333
|
-
internalGroup = internalGroups[0].concat(internalGroups[1])
|
|
334
|
-
.concat(internalGroups[2]);
|
|
335
|
-
straightGroup = groups[1];
|
|
336
|
-
diagonalGroups = groups[0].concat(groups[2]);
|
|
337
|
-
internalDist = [df.nearHorizonIsBetter, df.leftIsBetter];
|
|
338
|
-
straightDist = [df.nearHorizonIsBetter, df.leftIsBetter];
|
|
339
|
-
diagonalDist = [df.nearPlumbLineIsBetter, df.bottomIsBetter,
|
|
340
|
-
df.nearTargetLeftIsBetter];
|
|
341
|
-
// 合并时主轴=垂直最近,次轴=水平对齐
|
|
342
|
-
mergedDist = [df.nearHorizonIsBetter, df.nearPlumbLineIsBetter,
|
|
343
|
-
df.leftIsBetter];
|
|
344
|
-
break;
|
|
345
|
-
case 'down':
|
|
346
|
-
internalGroup = internalGroups[6].concat(internalGroups[7])
|
|
347
|
-
.concat(internalGroups[8]);
|
|
348
|
-
straightGroup = groups[7];
|
|
349
|
-
diagonalGroups = groups[6].concat(groups[8]);
|
|
350
|
-
internalDist = [df.nearHorizonIsBetter, df.leftIsBetter];
|
|
351
|
-
straightDist = [df.nearHorizonIsBetter, df.leftIsBetter];
|
|
352
|
-
diagonalDist = [df.nearPlumbLineIsBetter, df.topIsBetter,
|
|
353
|
-
df.nearTargetLeftIsBetter];
|
|
354
|
-
mergedDist = [df.nearHorizonIsBetter, df.nearPlumbLineIsBetter,
|
|
355
|
-
df.leftIsBetter];
|
|
356
|
-
break;
|
|
357
|
-
default:
|
|
358
|
-
return null;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
if (preferNearest) {
|
|
362
|
-
// 跨 section:直线与斜向候选合并为一层按距离竞争,最近一行/一列胜出,
|
|
363
|
-
// 修复「远处同列元素抢焦点」(如按下从按钮行越过相邻行跳到远处对齐元素)。
|
|
364
|
-
priorities = [
|
|
365
|
-
{ group: internalGroup, distance: internalDist },
|
|
366
|
-
{
|
|
367
|
-
group: config.straightOnly
|
|
368
|
-
? straightGroup
|
|
369
|
-
: straightGroup.concat(diagonalGroups),
|
|
370
|
-
distance: mergedDist
|
|
371
|
-
}
|
|
372
|
-
];
|
|
373
|
-
} else {
|
|
374
|
-
// section 内:保留上游分层(直线候选强于斜向),保证同行/同列相邻项优先,
|
|
375
|
-
// 不被「不同行/不同列但像素更近」的元素抢走(如变宽卡片墙里同行相邻卡)。
|
|
376
|
-
priorities = [
|
|
377
|
-
{ group: internalGroup, distance: internalDist },
|
|
378
|
-
{ group: straightGroup, distance: straightDist },
|
|
379
|
-
{ group: diagonalGroups, distance: diagonalDist }
|
|
380
|
-
];
|
|
381
|
-
if (config.straightOnly) {
|
|
382
|
-
priorities.pop();
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
var destGroup = prioritize(priorities);
|
|
387
|
-
if (!destGroup) {
|
|
388
|
-
return null;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
var dest = null;
|
|
288
|
+
// 3) rememberSource:previous.target 仍是合法候选且不被 best 严格击败 → 复焦,
|
|
289
|
+
// 保留「左右往返不抖动」语义(Android 原生无此机制,此处嫁接)。
|
|
392
290
|
if (config.rememberSource &&
|
|
393
291
|
config.previous &&
|
|
394
292
|
config.previous.destination === target &&
|
|
395
293
|
config.previous.reverse === direction) {
|
|
396
|
-
for (var j = 0; j <
|
|
397
|
-
if (
|
|
398
|
-
|
|
294
|
+
for (var j = 0; j < rects.length; j++) {
|
|
295
|
+
if (rects[j].element === config.previous.target) {
|
|
296
|
+
if (!snIsBetterCandidate(direction, targetRect, best, rects[j])) {
|
|
297
|
+
return rects[j].element;
|
|
298
|
+
}
|
|
399
299
|
break;
|
|
400
300
|
}
|
|
401
301
|
}
|
|
402
302
|
}
|
|
403
303
|
|
|
404
|
-
|
|
405
|
-
dest = destGroup[0].element;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
return dest;
|
|
304
|
+
return best.element;
|
|
409
305
|
}
|
|
410
306
|
|
|
411
307
|
/********************/
|
|
@@ -770,6 +666,50 @@ var $: any = null;
|
|
|
770
666
|
return navigate(target, direction, candidates, config, preferNearest);
|
|
771
667
|
}
|
|
772
668
|
|
|
669
|
+
// section 级方向剪枝:跨区收集候选前,按「该 section 所有项的 union 包围盒」
|
|
670
|
+
// 过 Android isCandidate 方向门,只剪掉「纯反方向」section(如按左时整个在右侧的)。
|
|
671
|
+
// beam 内/外不在此区分,留给项级 snBeamBeats。容器/空 section 天然跳过。
|
|
672
|
+
// sectionNavMap:focusNext 已构造的 { sectionId: navigableElements[] },复用不重查。
|
|
673
|
+
function filterCandidatesByDirection(source, direction, sectionNavMap, excludeSectionId) {
|
|
674
|
+
var srcRect = getRect(source);
|
|
675
|
+
var kept = [];
|
|
676
|
+
for (var id in sectionNavMap) {
|
|
677
|
+
if (id === excludeSectionId) {
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
var elems = sectionNavMap[id];
|
|
681
|
+
if (!elems || !elems.length) {
|
|
682
|
+
continue; // 容器 section / 空 section 跳过
|
|
683
|
+
}
|
|
684
|
+
var left = Infinity, top = Infinity, right = -Infinity, bottom = -Infinity;
|
|
685
|
+
var rects = [];
|
|
686
|
+
for (var i = 0; i < elems.length; i++) {
|
|
687
|
+
var r = getRect(elems[i]);
|
|
688
|
+
if (!r) {
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
rects.push(r);
|
|
692
|
+
if (r.left < left) { left = r.left; }
|
|
693
|
+
if (r.top < top) { top = r.top; }
|
|
694
|
+
if (r.right > right) { right = r.right; }
|
|
695
|
+
if (r.bottom > bottom) { bottom = r.bottom; }
|
|
696
|
+
}
|
|
697
|
+
if (!rects.length) {
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
var unionRect = {
|
|
701
|
+
left: left, top: top, right: right, bottom: bottom,
|
|
702
|
+
width: right - left, height: bottom - top
|
|
703
|
+
};
|
|
704
|
+
if (snIsCandidate(srcRect, unionRect, direction)) {
|
|
705
|
+
for (var j = 0; j < rects.length; j++) {
|
|
706
|
+
kept.push(rects[j].element);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return kept;
|
|
711
|
+
}
|
|
712
|
+
|
|
773
713
|
function focusNext(direction, currentFocusedElement, currentSectionId) {
|
|
774
714
|
var extSelector =
|
|
775
715
|
currentFocusedElement.getAttribute('data-sn-' + direction);
|
|
@@ -805,11 +745,24 @@ var $: any = null;
|
|
|
805
745
|
);
|
|
806
746
|
|
|
807
747
|
if (!next && config.restrict == 'self-first') {
|
|
808
|
-
// 离开本 section
|
|
748
|
+
// 离开本 section 跨区查找:先做 section 级方向剪枝(剪掉纯反方向 section),
|
|
749
|
+
// 再走滚动容器感知 + 项级 Android 打分。
|
|
750
|
+
var prunedCandidates = filterCandidatesByDirection(
|
|
751
|
+
currentFocusedElement,
|
|
752
|
+
direction,
|
|
753
|
+
sectionNavigableElements,
|
|
754
|
+
currentSectionId
|
|
755
|
+
);
|
|
756
|
+
if (!prunedCandidates.length) {
|
|
757
|
+
// 剪光兜底:回退全量(极端布局下某方向无任何 section 过门时不退化为死焦点)
|
|
758
|
+
prunedCandidates = exclude(
|
|
759
|
+
allNavigableElements, currentSectionNavigableElements
|
|
760
|
+
);
|
|
761
|
+
}
|
|
809
762
|
next = navigateWithinScrollScope(
|
|
810
763
|
currentFocusedElement,
|
|
811
764
|
direction,
|
|
812
|
-
|
|
765
|
+
prunedCandidates,
|
|
813
766
|
config,
|
|
814
767
|
true
|
|
815
768
|
);
|