Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
7859 views
1
package com.artifex.mupdfdemo;
2
3
import java.util.LinkedList;
4
import java.util.NoSuchElementException;
5
6
import android.content.Context;
7
import android.graphics.Point;
8
import android.graphics.Rect;
9
import android.util.AttributeSet;
10
import android.util.SparseArray;
11
import android.view.GestureDetector;
12
import android.view.MotionEvent;
13
import android.view.ScaleGestureDetector;
14
import android.view.View;
15
import android.widget.Adapter;
16
import android.widget.AdapterView;
17
import android.widget.Scroller;
18
19
public class ReaderView
20
extends AdapterView<Adapter>
21
implements GestureDetector.OnGestureListener, ScaleGestureDetector.OnScaleGestureListener, Runnable {
22
private static final int MOVING_DIAGONALLY = 0;
23
private static final int MOVING_LEFT = 1;
24
private static final int MOVING_RIGHT = 2;
25
private static final int MOVING_UP = 3;
26
private static final int MOVING_DOWN = 4;
27
28
private static final int FLING_MARGIN = 100;
29
private static final int GAP = 20;
30
31
private static final float MIN_SCALE = 1.0f;
32
private static final float MAX_SCALE = 5.0f;
33
private static final float REFLOW_SCALE_FACTOR = 0.5f;
34
35
private static final boolean HORIZONTAL_SCROLLING = true;
36
37
private Adapter mAdapter;
38
private int mCurrent; // Adapter's index for the current view
39
private boolean mResetLayout;
40
private final SparseArray<View>
41
mChildViews = new SparseArray<View>(3);
42
// Shadows the children of the adapter view
43
// but with more sensible indexing
44
private final LinkedList<View>
45
mViewCache = new LinkedList<View>();
46
private boolean mUserInteracting; // Whether the user is interacting
47
private boolean mScaling; // Whether the user is currently pinch zooming
48
private float mScale = 1.0f;
49
private int mXScroll; // Scroll amounts recorded from events.
50
private int mYScroll; // and then accounted for in onLayout
51
private boolean mReflow = false;
52
private boolean mReflowChanged = false;
53
private final GestureDetector
54
mGestureDetector;
55
private final ScaleGestureDetector
56
mScaleGestureDetector;
57
private final Scroller mScroller;
58
private final Stepper mStepper;
59
private int mScrollerLastX;
60
private int mScrollerLastY;
61
private float mLastScaleFocusX;
62
private float mLastScaleFocusY;
63
64
static abstract class ViewMapper {
65
abstract void applyToView(View view);
66
}
67
68
public ReaderView(Context context) {
69
super(context);
70
mGestureDetector = new GestureDetector(this);
71
mScaleGestureDetector = new ScaleGestureDetector(context, this);
72
mScroller = new Scroller(context);
73
mStepper = new Stepper(this, this);
74
}
75
76
public ReaderView(Context context, AttributeSet attrs) {
77
super(context, attrs);
78
79
// "Edit mode" means when the View is being displayed in the Android GUI editor. (this class
80
// is instantiated in the IDE, so we need to be a bit careful what we do).
81
if (isInEditMode())
82
{
83
mGestureDetector = null;
84
mScaleGestureDetector = null;
85
mScroller = null;
86
mStepper = null;
87
}
88
else
89
{
90
mGestureDetector = new GestureDetector(this);
91
mScaleGestureDetector = new ScaleGestureDetector(context, this);
92
mScroller = new Scroller(context);
93
mStepper = new Stepper(this, this);
94
}
95
}
96
97
public ReaderView(Context context, AttributeSet attrs, int defStyle) {
98
super(context, attrs, defStyle);
99
mGestureDetector = new GestureDetector(this);
100
mScaleGestureDetector = new ScaleGestureDetector(context, this);
101
mScroller = new Scroller(context);
102
mStepper = new Stepper(this, this);
103
}
104
105
public int getDisplayedViewIndex() {
106
return mCurrent;
107
}
108
109
public void setDisplayedViewIndex(int i) {
110
if (0 <= i && i < mAdapter.getCount()) {
111
onMoveOffChild(mCurrent);
112
mCurrent = i;
113
onMoveToChild(i);
114
mResetLayout = true;
115
requestLayout();
116
}
117
}
118
119
public void moveToNext() {
120
View v = mChildViews.get(mCurrent+1);
121
if (v != null)
122
slideViewOntoScreen(v);
123
}
124
125
public void moveToPrevious() {
126
View v = mChildViews.get(mCurrent-1);
127
if (v != null)
128
slideViewOntoScreen(v);
129
}
130
131
// When advancing down the page, we want to advance by about
132
// 90% of a screenful. But we'd be happy to advance by between
133
// 80% and 95% if it means we hit the bottom in a whole number
134
// of steps.
135
private int smartAdvanceAmount(int screenHeight, int max) {
136
int advance = (int)(screenHeight * 0.9 + 0.5);
137
int leftOver = max % advance;
138
int steps = max / advance;
139
if (leftOver == 0) {
140
// We'll make it exactly. No adjustment
141
} else if ((float)leftOver / steps <= screenHeight * 0.05) {
142
// We can adjust up by less than 5% to make it exact.
143
advance += (int)((float)leftOver/steps + 0.5);
144
} else {
145
int overshoot = advance - leftOver;
146
if ((float)overshoot / steps <= screenHeight * 0.1) {
147
// We can adjust down by less than 10% to make it exact.
148
advance -= (int)((float)overshoot/steps + 0.5);
149
}
150
}
151
if (advance > max)
152
advance = max;
153
return advance;
154
}
155
156
public void smartMoveForwards() {
157
View v = mChildViews.get(mCurrent);
158
if (v == null)
159
return;
160
161
// The following code works in terms of where the screen is on the views;
162
// so for example, if the currentView is at (-100,-100), the visible
163
// region would be at (100,100). If the previous page was (2000, 3000) in
164
// size, the visible region of the previous page might be (2100 + GAP, 100)
165
// (i.e. off the previous page). This is different to the way the rest of
166
// the code in this file is written, but it's easier for me to think about.
167
// At some point we may refactor this to fit better with the rest of the
168
// code.
169
170
// screenWidth/Height are the actual width/height of the screen. e.g. 480/800
171
int screenWidth = getWidth();
172
int screenHeight = getHeight();
173
// We might be mid scroll; we want to calculate where we scroll to based on
174
// where this scroll would end, not where we are now (to allow for people
175
// bashing 'forwards' very fast.
176
int remainingX = mScroller.getFinalX() - mScroller.getCurrX();
177
int remainingY = mScroller.getFinalY() - mScroller.getCurrY();
178
// right/bottom is in terms of pixels within the scaled document; e.g. 1000
179
int top = -(v.getTop() + mYScroll + remainingY);
180
int right = screenWidth -(v.getLeft() + mXScroll + remainingX);
181
int bottom = screenHeight+top;
182
// docWidth/Height are the width/height of the scaled document e.g. 2000x3000
183
int docWidth = v.getMeasuredWidth();
184
int docHeight = v.getMeasuredHeight();
185
186
int xOffset, yOffset;
187
if (bottom >= docHeight) {
188
// We are flush with the bottom. Advance to next column.
189
if (right + screenWidth > docWidth) {
190
// No room for another column - go to next page
191
View nv = mChildViews.get(mCurrent+1);
192
if (nv == null) // No page to advance to
193
return;
194
int nextTop = -(nv.getTop() + mYScroll + remainingY);
195
int nextLeft = -(nv.getLeft() + mXScroll + remainingX);
196
int nextDocWidth = nv.getMeasuredWidth();
197
int nextDocHeight = nv.getMeasuredHeight();
198
199
// Allow for the next page maybe being shorter than the screen is high
200
yOffset = (nextDocHeight < screenHeight ? ((nextDocHeight - screenHeight)>>1) : 0);
201
202
if (nextDocWidth < screenWidth) {
203
// Next page is too narrow to fill the screen. Scroll to the top, centred.
204
xOffset = (nextDocWidth - screenWidth)>>1;
205
} else {
206
// Reset X back to the left hand column
207
xOffset = right % screenWidth;
208
// Adjust in case the previous page is less wide
209
if (xOffset + screenWidth > nextDocWidth)
210
xOffset = nextDocWidth - screenWidth;
211
}
212
xOffset -= nextLeft;
213
yOffset -= nextTop;
214
} else {
215
// Move to top of next column
216
xOffset = screenWidth;
217
yOffset = screenHeight - bottom;
218
}
219
} else {
220
// Advance by 90% of the screen height downwards (in case lines are partially cut off)
221
xOffset = 0;
222
yOffset = smartAdvanceAmount(screenHeight, docHeight - bottom);
223
}
224
mScrollerLastX = mScrollerLastY = 0;
225
mScroller.startScroll(0, 0, remainingX - xOffset, remainingY - yOffset, 400);
226
mStepper.prod();
227
}
228
229
public void smartMoveBackwards() {
230
View v = mChildViews.get(mCurrent);
231
if (v == null)
232
return;
233
234
// The following code works in terms of where the screen is on the views;
235
// so for example, if the currentView is at (-100,-100), the visible
236
// region would be at (100,100). If the previous page was (2000, 3000) in
237
// size, the visible region of the previous page might be (2100 + GAP, 100)
238
// (i.e. off the previous page). This is different to the way the rest of
239
// the code in this file is written, but it's easier for me to think about.
240
// At some point we may refactor this to fit better with the rest of the
241
// code.
242
243
// screenWidth/Height are the actual width/height of the screen. e.g. 480/800
244
int screenWidth = getWidth();
245
int screenHeight = getHeight();
246
// We might be mid scroll; we want to calculate where we scroll to based on
247
// where this scroll would end, not where we are now (to allow for people
248
// bashing 'forwards' very fast.
249
int remainingX = mScroller.getFinalX() - mScroller.getCurrX();
250
int remainingY = mScroller.getFinalY() - mScroller.getCurrY();
251
// left/top is in terms of pixels within the scaled document; e.g. 1000
252
int left = -(v.getLeft() + mXScroll + remainingX);
253
int top = -(v.getTop() + mYScroll + remainingY);
254
// docWidth/Height are the width/height of the scaled document e.g. 2000x3000
255
int docHeight = v.getMeasuredHeight();
256
257
int xOffset, yOffset;
258
if (top <= 0) {
259
// We are flush with the top. Step back to previous column.
260
if (left < screenWidth) {
261
/* No room for previous column - go to previous page */
262
View pv = mChildViews.get(mCurrent-1);
263
if (pv == null) /* No page to advance to */
264
return;
265
int prevDocWidth = pv.getMeasuredWidth();
266
int prevDocHeight = pv.getMeasuredHeight();
267
268
// Allow for the next page maybe being shorter than the screen is high
269
yOffset = (prevDocHeight < screenHeight ? ((prevDocHeight - screenHeight)>>1) : 0);
270
271
int prevLeft = -(pv.getLeft() + mXScroll);
272
int prevTop = -(pv.getTop() + mYScroll);
273
if (prevDocWidth < screenWidth) {
274
// Previous page is too narrow to fill the screen. Scroll to the bottom, centred.
275
xOffset = (prevDocWidth - screenWidth)>>1;
276
} else {
277
// Reset X back to the right hand column
278
xOffset = (left > 0 ? left % screenWidth : 0);
279
if (xOffset + screenWidth > prevDocWidth)
280
xOffset = prevDocWidth - screenWidth;
281
while (xOffset + screenWidth*2 < prevDocWidth)
282
xOffset += screenWidth;
283
}
284
xOffset -= prevLeft;
285
yOffset -= prevTop-prevDocHeight+screenHeight;
286
} else {
287
// Move to bottom of previous column
288
xOffset = -screenWidth;
289
yOffset = docHeight - screenHeight + top;
290
}
291
} else {
292
// Retreat by 90% of the screen height downwards (in case lines are partially cut off)
293
xOffset = 0;
294
yOffset = -smartAdvanceAmount(screenHeight, top);
295
}
296
mScrollerLastX = mScrollerLastY = 0;
297
mScroller.startScroll(0, 0, remainingX - xOffset, remainingY - yOffset, 400);
298
mStepper.prod();
299
}
300
301
public void resetupChildren() {
302
for (int i = 0; i < mChildViews.size(); i++)
303
onChildSetup(mChildViews.keyAt(i), mChildViews.valueAt(i));
304
}
305
306
public void applyToChildren(ViewMapper mapper) {
307
for (int i = 0; i < mChildViews.size(); i++)
308
mapper.applyToView(mChildViews.valueAt(i));
309
}
310
311
public void refresh(boolean reflow) {
312
mReflow = reflow;
313
mReflowChanged = true;
314
mResetLayout = true;
315
316
mScale = 1.0f;
317
mXScroll = mYScroll = 0;
318
319
requestLayout();
320
}
321
322
protected void onChildSetup(int i, View v) {}
323
324
protected void onMoveToChild(int i) {}
325
326
protected void onMoveOffChild(int i) {}
327
328
protected void onSettle(View v) {};
329
330
protected void onUnsettle(View v) {};
331
332
protected void onNotInUse(View v) {};
333
334
protected void onScaleChild(View v, Float scale) {};
335
336
public View getView(int i) {
337
return mChildViews.get(i);
338
}
339
340
public View getDisplayedView() {
341
return mChildViews.get(mCurrent);
342
}
343
344
public void run() {
345
if (!mScroller.isFinished()) {
346
mScroller.computeScrollOffset();
347
int x = mScroller.getCurrX();
348
int y = mScroller.getCurrY();
349
mXScroll += x - mScrollerLastX;
350
mYScroll += y - mScrollerLastY;
351
mScrollerLastX = x;
352
mScrollerLastY = y;
353
requestLayout();
354
mStepper.prod();
355
}
356
else if (!mUserInteracting) {
357
// End of an inertial scroll and the user is not interacting.
358
// The layout is stable
359
View v = mChildViews.get(mCurrent);
360
if (v != null)
361
postSettle(v);
362
}
363
}
364
365
public boolean onDown(MotionEvent arg0) {
366
mScroller.forceFinished(true);
367
return true;
368
}
369
370
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
371
float velocityY) {
372
if (mScaling)
373
return true;
374
375
View v = mChildViews.get(mCurrent);
376
if (v != null) {
377
Rect bounds = getScrollBounds(v);
378
switch(directionOfTravel(velocityX, velocityY)) {
379
case MOVING_LEFT:
380
if (HORIZONTAL_SCROLLING && bounds.left >= 0) {
381
// Fling off to the left bring next view onto screen
382
View vl = mChildViews.get(mCurrent+1);
383
384
if (vl != null) {
385
slideViewOntoScreen(vl);
386
return true;
387
}
388
}
389
break;
390
case MOVING_UP:
391
if (!HORIZONTAL_SCROLLING && bounds.top >= 0) {
392
// Fling off to the top bring next view onto screen
393
View vl = mChildViews.get(mCurrent+1);
394
395
if (vl != null) {
396
slideViewOntoScreen(vl);
397
return true;
398
}
399
}
400
break;
401
case MOVING_RIGHT:
402
if (HORIZONTAL_SCROLLING && bounds.right <= 0) {
403
// Fling off to the right bring previous view onto screen
404
View vr = mChildViews.get(mCurrent-1);
405
406
if (vr != null) {
407
slideViewOntoScreen(vr);
408
return true;
409
}
410
}
411
break;
412
case MOVING_DOWN:
413
if (!HORIZONTAL_SCROLLING && bounds.bottom <= 0) {
414
// Fling off to the bottom bring previous view onto screen
415
View vr = mChildViews.get(mCurrent-1);
416
417
if (vr != null) {
418
slideViewOntoScreen(vr);
419
return true;
420
}
421
}
422
break;
423
}
424
mScrollerLastX = mScrollerLastY = 0;
425
// If the page has been dragged out of bounds then we want to spring back
426
// nicely. fling jumps back into bounds instantly, so we don't want to use
427
// fling in that case. On the other hand, we don't want to forgo a fling
428
// just because of a slightly off-angle drag taking us out of bounds other
429
// than in the direction of the drag, so we test for out of bounds only
430
// in the direction of travel.
431
//
432
// Also don't fling if out of bounds in any direction by more than fling
433
// margin
434
Rect expandedBounds = new Rect(bounds);
435
expandedBounds.inset(-FLING_MARGIN, -FLING_MARGIN);
436
437
if(withinBoundsInDirectionOfTravel(bounds, velocityX, velocityY)
438
&& expandedBounds.contains(0, 0)) {
439
mScroller.fling(0, 0, (int)velocityX, (int)velocityY, bounds.left, bounds.right, bounds.top, bounds.bottom);
440
mStepper.prod();
441
}
442
}
443
444
return true;
445
}
446
447
public void onLongPress(MotionEvent e) {
448
}
449
450
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
451
float distanceY) {
452
if (!mScaling) {
453
mXScroll -= distanceX;
454
mYScroll -= distanceY;
455
requestLayout();
456
}
457
return true;
458
}
459
460
public void onShowPress(MotionEvent e) {
461
}
462
463
public boolean onSingleTapUp(MotionEvent e) {
464
return false;
465
}
466
467
public boolean onScale(ScaleGestureDetector detector) {
468
float previousScale = mScale;
469
float scale_factor = mReflow ? REFLOW_SCALE_FACTOR : 1.0f;
470
float min_scale = MIN_SCALE * scale_factor;
471
float max_scale = MAX_SCALE * scale_factor;
472
mScale = Math.min(Math.max(mScale * detector.getScaleFactor(), min_scale), max_scale);
473
474
if (mReflow) {
475
View v = mChildViews.get(mCurrent);
476
if (v != null)
477
onScaleChild(v, mScale);
478
} else {
479
float factor = mScale/previousScale;
480
481
View v = mChildViews.get(mCurrent);
482
if (v != null) {
483
float currentFocusX = detector.getFocusX();
484
float currentFocusY = detector.getFocusY();
485
// Work out the focus point relative to the view top left
486
int viewFocusX = (int)currentFocusX - (v.getLeft() + mXScroll);
487
int viewFocusY = (int)currentFocusY - (v.getTop() + mYScroll);
488
// Scroll to maintain the focus point
489
mXScroll += viewFocusX - viewFocusX * factor;
490
mYScroll += viewFocusY - viewFocusY * factor;
491
492
if (mLastScaleFocusX>=0)
493
mXScroll+=currentFocusX-mLastScaleFocusX;
494
if (mLastScaleFocusY>=0)
495
mYScroll+=currentFocusY-mLastScaleFocusY;
496
497
mLastScaleFocusX=currentFocusX;
498
mLastScaleFocusY=currentFocusY;
499
requestLayout();
500
}
501
}
502
return true;
503
}
504
505
public boolean onScaleBegin(ScaleGestureDetector detector) {
506
mScaling = true;
507
// Ignore any scroll amounts yet to be accounted for: the
508
// screen is not showing the effect of them, so they can
509
// only confuse the user
510
mXScroll = mYScroll = 0;
511
mLastScaleFocusX = mLastScaleFocusY = -1;
512
return true;
513
}
514
515
public void onScaleEnd(ScaleGestureDetector detector) {
516
if (mReflow) {
517
applyToChildren(new ViewMapper() {
518
@Override
519
void applyToView(View view) {
520
onScaleChild(view, mScale);
521
}
522
});
523
}
524
mScaling = false;
525
}
526
527
@Override
528
public boolean onTouchEvent(MotionEvent event) {
529
mScaleGestureDetector.onTouchEvent(event);
530
mGestureDetector.onTouchEvent(event);
531
532
if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) {
533
mUserInteracting = true;
534
}
535
if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) {
536
mUserInteracting = false;
537
538
View v = mChildViews.get(mCurrent);
539
if (v != null) {
540
if (mScroller.isFinished()) {
541
// If, at the end of user interaction, there is no
542
// current inertial scroll in operation then animate
543
// the view onto screen if necessary
544
slideViewOntoScreen(v);
545
}
546
547
if (mScroller.isFinished()) {
548
// If still there is no inertial scroll in operation
549
// then the layout is stable
550
postSettle(v);
551
}
552
}
553
}
554
555
return true;
556
}
557
558
@Override
559
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
560
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
561
562
int n = getChildCount();
563
for (int i = 0; i < n; i++)
564
measureView(getChildAt(i));
565
}
566
567
@Override
568
protected void onLayout(boolean changed, int left, int top, int right,
569
int bottom) {
570
super.onLayout(changed, left, top, right, bottom);
571
572
// "Edit mode" means when the View is being displayed in the Android GUI editor. (this class
573
// is instantiated in the IDE, so we need to be a bit careful what we do).
574
if (isInEditMode())
575
return;
576
577
View cv = mChildViews.get(mCurrent);
578
Point cvOffset;
579
580
if (!mResetLayout) {
581
// Move to next or previous if current is sufficiently off center
582
if (cv != null) {
583
boolean move;
584
cvOffset = subScreenSizeOffset(cv);
585
// cv.getRight() may be out of date with the current scale
586
// so add left to the measured width for the correct position
587
if (HORIZONTAL_SCROLLING)
588
move = cv.getLeft() + cv.getMeasuredWidth() + cvOffset.x + GAP/2 + mXScroll < getWidth()/2;
589
else
590
move = cv.getTop() + cv.getMeasuredHeight() + cvOffset.y + GAP/2 + mYScroll < getHeight()/2;
591
if (move && mCurrent + 1 < mAdapter.getCount()) {
592
postUnsettle(cv);
593
// post to invoke test for end of animation
594
// where we must set hq area for the new current view
595
mStepper.prod();
596
597
onMoveOffChild(mCurrent);
598
mCurrent++;
599
onMoveToChild(mCurrent);
600
}
601
602
if (HORIZONTAL_SCROLLING)
603
move = cv.getLeft() - cvOffset.x - GAP/2 + mXScroll >= getWidth()/2;
604
else
605
move = cv.getTop() - cvOffset.y - GAP/2 + mYScroll >= getHeight()/2;
606
if (move && mCurrent > 0) {
607
postUnsettle(cv);
608
// post to invoke test for end of animation
609
// where we must set hq area for the new current view
610
mStepper.prod();
611
612
onMoveOffChild(mCurrent);
613
mCurrent--;
614
onMoveToChild(mCurrent);
615
}
616
}
617
618
// Remove not needed children and hold them for reuse
619
int numChildren = mChildViews.size();
620
int childIndices[] = new int[numChildren];
621
for (int i = 0; i < numChildren; i++)
622
childIndices[i] = mChildViews.keyAt(i);
623
624
for (int i = 0; i < numChildren; i++) {
625
int ai = childIndices[i];
626
if (ai < mCurrent - 1 || ai > mCurrent + 1) {
627
View v = mChildViews.get(ai);
628
onNotInUse(v);
629
mViewCache.add(v);
630
removeViewInLayout(v);
631
mChildViews.remove(ai);
632
}
633
}
634
} else {
635
mResetLayout = false;
636
mXScroll = mYScroll = 0;
637
638
// Remove all children and hold them for reuse
639
int numChildren = mChildViews.size();
640
for (int i = 0; i < numChildren; i++) {
641
View v = mChildViews.valueAt(i);
642
onNotInUse(v);
643
mViewCache.add(v);
644
removeViewInLayout(v);
645
}
646
mChildViews.clear();
647
648
// Don't reuse cached views if the adapter has changed
649
if (mReflowChanged) {
650
mReflowChanged = false;
651
mViewCache.clear();
652
}
653
654
// post to ensure generation of hq area
655
mStepper.prod();
656
}
657
658
// Ensure current view is present
659
int cvLeft, cvRight, cvTop, cvBottom;
660
boolean notPresent = (mChildViews.get(mCurrent) == null);
661
cv = getOrCreateChild(mCurrent);
662
// When the view is sub-screen-size in either dimension we
663
// offset it to center within the screen area, and to keep
664
// the views spaced out
665
cvOffset = subScreenSizeOffset(cv);
666
if (notPresent) {
667
//Main item not already present. Just place it top left
668
cvLeft = cvOffset.x;
669
cvTop = cvOffset.y;
670
} else {
671
// Main item already present. Adjust by scroll offsets
672
cvLeft = cv.getLeft() + mXScroll;
673
cvTop = cv.getTop() + mYScroll;
674
}
675
// Scroll values have been accounted for
676
mXScroll = mYScroll = 0;
677
cvRight = cvLeft + cv.getMeasuredWidth();
678
cvBottom = cvTop + cv.getMeasuredHeight();
679
680
if (!mUserInteracting && mScroller.isFinished()) {
681
Point corr = getCorrection(getScrollBounds(cvLeft, cvTop, cvRight, cvBottom));
682
cvRight += corr.x;
683
cvLeft += corr.x;
684
cvTop += corr.y;
685
cvBottom += corr.y;
686
} else if (HORIZONTAL_SCROLLING && cv.getMeasuredHeight() <= getHeight()) {
687
// When the current view is as small as the screen in height, clamp
688
// it vertically
689
Point corr = getCorrection(getScrollBounds(cvLeft, cvTop, cvRight, cvBottom));
690
cvTop += corr.y;
691
cvBottom += corr.y;
692
} else if (!HORIZONTAL_SCROLLING && cv.getMeasuredWidth() <= getWidth()) {
693
// When the current view is as small as the screen in width, clamp
694
// it horizontally
695
Point corr = getCorrection(getScrollBounds(cvLeft, cvTop, cvRight, cvBottom));
696
cvRight += corr.x;
697
cvLeft += corr.x;
698
}
699
700
cv.layout(cvLeft, cvTop, cvRight, cvBottom);
701
702
if (mCurrent > 0) {
703
View lv = getOrCreateChild(mCurrent - 1);
704
Point leftOffset = subScreenSizeOffset(lv);
705
if (HORIZONTAL_SCROLLING)
706
{
707
int gap = leftOffset.x + GAP + cvOffset.x;
708
lv.layout(cvLeft - lv.getMeasuredWidth() - gap,
709
(cvBottom + cvTop - lv.getMeasuredHeight())/2,
710
cvLeft - gap,
711
(cvBottom + cvTop + lv.getMeasuredHeight())/2);
712
} else {
713
int gap = leftOffset.y + GAP + cvOffset.y;
714
lv.layout((cvLeft + cvRight - lv.getMeasuredWidth())/2,
715
cvTop - lv.getMeasuredHeight() - gap,
716
(cvLeft + cvRight + lv.getMeasuredWidth())/2,
717
cvTop - gap);
718
}
719
}
720
721
if (mCurrent + 1 < mAdapter.getCount()) {
722
View rv = getOrCreateChild(mCurrent + 1);
723
Point rightOffset = subScreenSizeOffset(rv);
724
if (HORIZONTAL_SCROLLING)
725
{
726
int gap = cvOffset.x + GAP + rightOffset.x;
727
rv.layout(cvRight + gap,
728
(cvBottom + cvTop - rv.getMeasuredHeight())/2,
729
cvRight + rv.getMeasuredWidth() + gap,
730
(cvBottom + cvTop + rv.getMeasuredHeight())/2);
731
} else {
732
int gap = cvOffset.y + GAP + rightOffset.y;
733
rv.layout((cvLeft + cvRight - rv.getMeasuredWidth())/2,
734
cvBottom + gap,
735
(cvLeft + cvRight + rv.getMeasuredWidth())/2,
736
cvBottom + gap + rv.getMeasuredHeight());
737
}
738
}
739
740
invalidate();
741
}
742
743
@Override
744
public Adapter getAdapter() {
745
return mAdapter;
746
}
747
748
@Override
749
public View getSelectedView() {
750
return null;
751
}
752
753
@Override
754
public void setAdapter(Adapter adapter) {
755
mAdapter = adapter;
756
757
requestLayout();
758
}
759
760
@Override
761
public void setSelection(int arg0) {
762
throw new UnsupportedOperationException(getContext().getString(R.string.not_supported));
763
}
764
765
private View getCached() {
766
if (mViewCache.size() == 0)
767
return null;
768
else
769
return mViewCache.removeFirst();
770
}
771
772
private View getOrCreateChild(int i) {
773
View v = mChildViews.get(i);
774
if (v == null) {
775
v = mAdapter.getView(i, getCached(), this);
776
addAndMeasureChild(i, v);
777
onChildSetup(i, v);
778
onScaleChild(v, mScale);
779
}
780
781
return v;
782
}
783
784
private void addAndMeasureChild(int i, View v) {
785
LayoutParams params = v.getLayoutParams();
786
if (params == null) {
787
params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
788
}
789
addViewInLayout(v, 0, params, true);
790
mChildViews.append(i, v); // Record the view against it's adapter index
791
measureView(v);
792
}
793
794
private void measureView(View v) {
795
// See what size the view wants to be
796
v.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
797
798
if (!mReflow) {
799
// Work out a scale that will fit it to this view
800
float scale = Math.min((float)getWidth()/(float)v.getMeasuredWidth(),
801
(float)getHeight()/(float)v.getMeasuredHeight());
802
// Use the fitting values scaled by our current scale factor
803
v.measure(View.MeasureSpec.EXACTLY | (int)(v.getMeasuredWidth()*scale*mScale),
804
View.MeasureSpec.EXACTLY | (int)(v.getMeasuredHeight()*scale*mScale));
805
} else {
806
v.measure(View.MeasureSpec.EXACTLY | (int)(v.getMeasuredWidth()),
807
View.MeasureSpec.EXACTLY | (int)(v.getMeasuredHeight()));
808
}
809
}
810
811
private Rect getScrollBounds(int left, int top, int right, int bottom) {
812
int xmin = getWidth() - right;
813
int xmax = -left;
814
int ymin = getHeight() - bottom;
815
int ymax = -top;
816
817
// In either dimension, if view smaller than screen then
818
// constrain it to be central
819
if (xmin > xmax) xmin = xmax = (xmin + xmax)/2;
820
if (ymin > ymax) ymin = ymax = (ymin + ymax)/2;
821
822
return new Rect(xmin, ymin, xmax, ymax);
823
}
824
825
private Rect getScrollBounds(View v) {
826
// There can be scroll amounts not yet accounted for in
827
// onLayout, so add mXScroll and mYScroll to the current
828
// positions when calculating the bounds.
829
return getScrollBounds(v.getLeft() + mXScroll,
830
v.getTop() + mYScroll,
831
v.getLeft() + v.getMeasuredWidth() + mXScroll,
832
v.getTop() + v.getMeasuredHeight() + mYScroll);
833
}
834
835
private Point getCorrection(Rect bounds) {
836
return new Point(Math.min(Math.max(0,bounds.left),bounds.right),
837
Math.min(Math.max(0,bounds.top),bounds.bottom));
838
}
839
840
private void postSettle(final View v) {
841
// onSettle and onUnsettle are posted so that the calls
842
// wont be executed until after the system has performed
843
// layout.
844
post (new Runnable() {
845
public void run () {
846
onSettle(v);
847
}
848
});
849
}
850
851
private void postUnsettle(final View v) {
852
post (new Runnable() {
853
public void run () {
854
onUnsettle(v);
855
}
856
});
857
}
858
859
private void slideViewOntoScreen(View v) {
860
Point corr = getCorrection(getScrollBounds(v));
861
if (corr.x != 0 || corr.y != 0) {
862
mScrollerLastX = mScrollerLastY = 0;
863
mScroller.startScroll(0, 0, corr.x, corr.y, 400);
864
mStepper.prod();
865
}
866
}
867
868
private Point subScreenSizeOffset(View v) {
869
return new Point(Math.max((getWidth() - v.getMeasuredWidth())/2, 0),
870
Math.max((getHeight() - v.getMeasuredHeight())/2, 0));
871
}
872
873
private static int directionOfTravel(float vx, float vy) {
874
if (Math.abs(vx) > 2 * Math.abs(vy))
875
return (vx > 0) ? MOVING_RIGHT : MOVING_LEFT;
876
else if (Math.abs(vy) > 2 * Math.abs(vx))
877
return (vy > 0) ? MOVING_DOWN : MOVING_UP;
878
else
879
return MOVING_DIAGONALLY;
880
}
881
882
private static boolean withinBoundsInDirectionOfTravel(Rect bounds, float vx, float vy) {
883
switch (directionOfTravel(vx, vy)) {
884
case MOVING_DIAGONALLY: return bounds.contains(0, 0);
885
case MOVING_LEFT: return bounds.left <= 0;
886
case MOVING_RIGHT: return bounds.right >= 0;
887
case MOVING_UP: return bounds.top <= 0;
888
case MOVING_DOWN: return bounds.bottom >= 0;
889
default: throw new NoSuchElementException();
890
}
891
}
892
}
893
894