Skip to content

Commit cfac9a6

Browse files
authored
Scrolling improvements (#3659)
* Added some scrolling improvements so that tensile drag is stiffer at the margins. This also includes fixes for the 3d spinner, and duration picker. https://www.reddit.com/r/cn1/comments/yglild/pickersetminutestep_is_not_working/ * Improved scrolling behaviour to make tensile motion and snap-to-grid snappier. Added methods Motion.getVelocity() and Motion.countAvailableVelocitySamplePoints()
1 parent 596beed commit cfac9a6

File tree

7 files changed

+224
-362
lines changed

7 files changed

+224
-362
lines changed

CodenameOne/src/com/codename1/ui/Component.java

+35-11
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
import com.codename1.ui.events.*;
3535
import com.codename1.ui.geom.Dimension;
3636
import com.codename1.ui.geom.Rectangle;
37-
import com.codename1.ui.geom.Shape;
3837
import com.codename1.ui.layouts.FlowLayout;
3938
import com.codename1.ui.plaf.*;
4039
import com.codename1.ui.util.EventDispatcher;
@@ -3286,7 +3285,7 @@ public int getScrollX() {
32863285

32873286
/**
32883287
* Indicates the Y position of the scrolling, this number is relative to the
3289-
* component position and so a position of 0 would indicate the x position
3288+
* component position and so a position of 0 would indicate the y position
32903289
* of the component.
32913290
*
32923291
* @return the Y position of the scrolling
@@ -4597,7 +4596,7 @@ void clearDrag() {
45974596
}
45984597
}
45994598
draggedMotionX = null;
4600-
draggedMotionY = null;
4599+
draggedMotionY = null;
46014600

46024601
Component parent = getParent();
46034602
if(parent != null){
@@ -5355,7 +5354,7 @@ boolean isScrollDecelerationMotionInProgress() {
53555354
void startTensile(int offset, int dest, boolean vertical) {
53565355
Motion draggedMotion;
53575356
if(tensileDragEnabled) {
5358-
draggedMotion = Motion.createDecelerationMotion(offset, dest, 500);
5357+
draggedMotion = Motion.createDecelerationMotion(offset, dest, 300);
53595358
draggedMotion.start();
53605359
} else {
53615360
draggedMotion = Motion.createLinearMotion(offset, dest, 0);
@@ -5688,7 +5687,6 @@ private void pointerReleaseImpl(int x, int y) {
56885687
}
56895688

56905689
private void pointerReleaseImpl(Component lead, int x, int y) {
5691-
56925690
if(restoreDragPercentage > -1) {
56935691
Display.getInstance().setDragStartPercentage(restoreDragPercentage);
56945692
}
@@ -6311,7 +6309,7 @@ protected int getGridPosX() {
63116309
boolean isTensileMotionInProgress() {
63126310
return draggedMotionY != null && !draggedMotionY.isFinished();
63136311
}
6314-
6312+
63156313
/**
63166314
* {@inheritDoc}
63176315
*/
@@ -6342,18 +6340,45 @@ public boolean animate() {
63426340
// change the variable directly for efficiency both in removing redundant
63436341
// repaints and scroll checks
63446342
int dragVal = draggedMotionY.getValue();
6345-
6343+
int iv = getInvisibleAreaUnderVKB();
6344+
int edge = (getScrollDimension().getHeight() - getHeight() + iv);
6345+
if (!draggedMotionY.isFinished()
6346+
&& draggedMotionY.isDecayMotion()
6347+
&& draggedMotionY.countAvailableVelocitySamplingPoints() > 1) {
6348+
final Motion origDraggedMotionY = draggedMotionY;
6349+
if (dragVal < 0) {
6350+
// Once past 0, decay motion is too slow. We need to hit it with heavy friction.
6351+
draggedMotionY = Motion.createFrictionMotion(
6352+
dragVal,
6353+
-getTensileLength(),
6354+
(int)origDraggedMotionY.getVelocity(),
6355+
0.01f
6356+
);
6357+
draggedMotionY.start();
6358+
origDraggedMotionY.finish();
6359+
} else if (snapToGrid
6360+
&& Math.abs(origDraggedMotionY.getVelocity()) * 1000 < CN.convertToPixels(5)) {
6361+
// If snapToGrid is enabled, the grid snap should take precendent if the drag is slower
6362+
// than some threshold.
6363+
draggedMotionY = Motion.createFrictionMotion(
6364+
dragVal,
6365+
origDraggedMotionY.getDestinationValue(),
6366+
(int)origDraggedMotionY.getVelocity(),
6367+
0.1f
6368+
);
6369+
draggedMotionY.start();
6370+
origDraggedMotionY.finish();
6371+
}
6372+
}
63466373
// this can't be a part of the parent if since we need the last value to arrive
63476374
if (draggedMotionY.isFinished()) {
63486375
if (dragVal < 0) {
63496376
startTensile(dragVal, 0, true);
63506377
} else {
6351-
int iv = getInvisibleAreaUnderVKB();
6352-
int edge = (getScrollDimension().getHeight() - getHeight() + iv);
63536378
if (dragVal > edge && edge > 0) {
63546379
startTensile(dragVal, getScrollDimension().getHeight() - getHeight() + iv, true);
63556380
} else {
6356-
if (snapToGrid && getScrollY() < edge && getScrollY() > 0) {
6381+
if (snapToGrid) {
63576382
boolean tVal = tensileDragEnabled;
63586383
tensileDragEnabled = true;
63596384
int dest = getGridPosY();
@@ -6375,7 +6400,6 @@ else if (dest != scroll) {
63756400
}
63766401
}
63776402
}
6378-
63796403
// special callback to scroll Y to allow developers to override the setScrollY method effectively
63806404
setScrollY(dragVal);
63816405
updateTensileHighlightIntensity(dragVal, getScrollDimension().getHeight() - getHeight() + getInvisibleAreaUnderVKB(), false);

CodenameOne/src/com/codename1/ui/animations/Motion.java

+87-71
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ public static void setSlowMotion(boolean aSlowMotion) {
7777
private double initVelocity, friction;
7878
private int lastReturnedValue;
7979
private int [] previousLastReturnedValue = new int[3];
80+
private long[] previousLastReturnedValueTime = new long[3];
8081
private long currentMotionTime = -1;
82+
private long previousCurrentMotionTime = -1;
8183
private float p0, p1, p2, p3;
8284

8385
/**
@@ -95,7 +97,8 @@ protected Motion(int sourceValue, int destinationValue, int duration) {
9597
if(slowMotion) {
9698
this.duration *= 50;
9799
}
98-
previousLastReturnedValue[0] = -1;
100+
previousLastReturnedValue[0] = -1;
101+
previousLastReturnedValueTime[0] = -1;
99102
}
100103

101104
/**
@@ -105,6 +108,7 @@ public void finish() {
105108
if(!isFinished()) {
106109
startTime = System.currentTimeMillis() - duration;
107110
currentMotionTime = -1;
111+
previousCurrentMotionTime = -1;
108112
}
109113
}
110114

@@ -120,15 +124,17 @@ protected Motion(int sourceValue, float initVelocity, float friction) {
120124
this.initVelocity = initVelocity;
121125
this.friction = friction;
122126
duration = (int) ((Math.abs(initVelocity)) / friction);
123-
previousLastReturnedValue[0] = -1;
127+
previousLastReturnedValue[0] = -1;
128+
previousLastReturnedValueTime[0] = -1;
124129
}
125130

126131
protected Motion(int sourceValue, double initVelocity, double friction) {
127132
this.sourceValue = sourceValue;
128133
this.initVelocity = initVelocity;
129134
this.friction = friction;
130135
duration = (int) ((Math.abs(initVelocity)) / friction);
131-
previousLastReturnedValue[0] = -1;
136+
previousLastReturnedValue[0] = -1;
137+
previousLastReturnedValueTime[0] = -1;
132138
}
133139

134140

@@ -265,6 +271,24 @@ public static Motion createDecelerationMotion(int sourceValue, int destinationVa
265271
deceleration.motionType = DECELERATION;
266272
return deceleration;
267273
}
274+
275+
/**
276+
* Creates a deceleration motion starting from the current position of another motion.
277+
*
278+
* @param motion the number from which we are starting (usually indicating animation start position)
279+
* @param maxDestinationValue The farthest position to allow motion to go.
280+
* @param maxDuration The longest that the duration is allowed to proceed for.
281+
* @return new motion object
282+
*/
283+
public static Motion createDecelerationMotionFrom(Motion motion, int maxDestinationValue, int maxDuration) {
284+
return createDecelerationMotion(
285+
motion.lastReturnedValue,
286+
motion.destinationValue < motion.sourceValue
287+
? Math.min(motion.destinationValue, maxDestinationValue)
288+
: Math.max(motion.destinationValue, maxDestinationValue),
289+
(int)Math.min(maxDuration, motion.duration - (System.currentTimeMillis() - motion.startTime))
290+
);
291+
}
268292

269293
/**
270294
* Creates a friction motion starting from source with initial speed and the friction
@@ -319,6 +343,7 @@ public long getCurrentMotionTime() {
319343
* @param currentMotionTime the time in milliseconds for the motion.
320344
*/
321345
public void setCurrentMotionTime(long currentMotionTime) {
346+
this.previousCurrentMotionTime = this.currentMotionTime;
322347
this.currentMotionTime = currentMotionTime;
323348

324349
// workaround allowing the motion to be restarted when manually setting the current time
@@ -327,6 +352,10 @@ public void setCurrentMotionTime(long currentMotionTime) {
327352
}
328353
}
329354

355+
public boolean isDecayMotion() {
356+
return motionType == EXPONENTIAL_DECAY;
357+
}
358+
330359
/**
331360
* Sets the start time of the motion
332361
*
@@ -414,69 +443,6 @@ private int getCubicValue() {
414443
return current;
415444
}
416445

417-
// private int[] values = new int[1000];
418-
// private int[] times = new int[1000];
419-
// private int vOff;
420-
//
421-
// /**
422-
// * Returns the value for the motion for the current clock time.
423-
// * The value is dependent on the Motion type.
424-
// *
425-
// * @return a value that is relative to the source value
426-
// */
427-
// public int getValue() {
428-
// int v = getValueImpl();
429-
// if(isFinished() && vOff > 0) {
430-
// System.out.println("initVelocity:\t"+initVelocity + "\tfriction:\t" + friction + "\tdestinationValue:\t" + destinationValue + "\tsourceValue:\t" + sourceValue);
431-
// System.out.println("Value\tTime");
432-
// for(int iter = 0 ; iter < vOff ; iter++) {
433-
// System.out.println("" + values[iter] + "\t" + times[iter]);
434-
// }
435-
// vOff = 0;
436-
// } else {
437-
// values[vOff] = v;
438-
// int time = (int) getCurrentMotionTime();
439-
// times[vOff] = time;
440-
//
441-
// vOff++;
442-
// }
443-
//
444-
// return v;
445-
// }
446-
//
447-
// /**
448-
// * Returns the value for the motion for the current clock time.
449-
// * The value is dependent on the Motion type.
450-
// *
451-
// * @return a value that is relative to the source value
452-
// */
453-
// private int getValueImpl() {
454-
// if(currentMotionTime > -1 && startTime > getCurrentMotionTime()) {
455-
// return sourceValue;
456-
// }
457-
// switch(motionType) {
458-
// case SPLINE:
459-
// lastReturnedValue = getSplineValue();
460-
// break;
461-
// case CUBIC:
462-
// lastReturnedValue = getCubicValue();
463-
// break;
464-
// case FRICTION:
465-
// lastReturnedValue = getFriction();
466-
// break;
467-
// case DECELERATION:
468-
// lastReturnedValue = getRubber();
469-
// break;
470-
// case COLOR_LINEAR:
471-
// lastReturnedValue = getColorLinear();
472-
// break;
473-
// default:
474-
// lastReturnedValue = getLinear();
475-
// break;
476-
// }
477-
// return lastReturnedValue;
478-
// }
479-
480446
/**
481447
* Returns the value for the motion for the current clock time.
482448
* The value is dependent on the Motion type.
@@ -489,8 +455,14 @@ public int getValue() {
489455
}
490456

491457
previousLastReturnedValue[0] = previousLastReturnedValue[1];
458+
previousLastReturnedValueTime[0] = previousLastReturnedValueTime[1];
492459
previousLastReturnedValue[1] = previousLastReturnedValue[2];
460+
previousLastReturnedValueTime[1] = previousLastReturnedValueTime[2];
493461
previousLastReturnedValue[2] = lastReturnedValue;
462+
previousLastReturnedValueTime[2] = previousCurrentMotionTime;
463+
if (previousCurrentMotionTime < 0) {
464+
previousCurrentMotionTime = getCurrentMotionTime();
465+
}
494466
switch(motionType) {
495467
case SPLINE:
496468
lastReturnedValue = getSplineValue();
@@ -516,6 +488,55 @@ public int getValue() {
516488
}
517489
return lastReturnedValue;
518490
}
491+
492+
/**
493+
* Gets an approximation of the current velocity in pixels per millisecond.
494+
*
495+
* <p>NOTE: If {@link #countAvailableVelocitySamplingPoints()} <= 1, then this method will always output {@literal 0}.
496+
* Therefore the output of this method only has meaning if {@link #countAvailableVelocitySamplingPoints()} > {@literal 0}</p>
497+
*
498+
* @return Current velocity in pixels per millisecond.
499+
* @since 8.0
500+
*/
501+
public double getVelocity() {
502+
final long localCurrentMotionTime = getCurrentMotionTime();
503+
final int lastReturnedValueLocal = lastReturnedValue;
504+
double velocity = 0;
505+
boolean firstIteration = true;
506+
for (int i=2; i>= 0; i--) {
507+
final long t = previousLastReturnedValueTime[i];
508+
if (t <= 0 || localCurrentMotionTime == t) {
509+
break;
510+
}
511+
final int valueAtT = previousLastReturnedValue[i];
512+
final double spotVelocity = (lastReturnedValueLocal - valueAtT) / (double)(localCurrentMotionTime - t);
513+
velocity = firstIteration ? spotVelocity : (velocity + spotVelocity)/2.0;
514+
firstIteration = false;
515+
}
516+
517+
return velocity;
518+
}
519+
520+
/**
521+
* Gets the number of sampling points that can be used by {@link #getVelocity()}. A minimum of 2 sampling
522+
* points are required for the result of {@link #getVelocity()} to have any meaning.
523+
*
524+
* @since 8.0
525+
* @return The number of sampling points that can be used by {@link #getVelocity()}.
526+
*/
527+
public int countAvailableVelocitySamplingPoints() {
528+
int count = 1;
529+
final long localCurrentMotionTime = getCurrentMotionTime();
530+
for (int i=2; i>= 0; i--) {
531+
final long t = previousLastReturnedValueTime[i];
532+
if (t <= 0 || localCurrentMotionTime == t) {
533+
break;
534+
}
535+
count++;
536+
}
537+
538+
return count;
539+
}
519540

520541
private int getLinear() {
521542
//make sure we reach the destination value.
@@ -600,9 +621,6 @@ private int getFriction() {
600621
}
601622

602623
private int getExponentialDecay() {
603-
//amplitude = initialVelocity * scaleFactor;
604-
//targetPosition = position + amplitude;
605-
//timestamp = Date.now();
606624
double elapsed = getCurrentMotionTime();
607625
double timeConstant = friction;
608626
double amplitude = targetPosition - sourceValue;
@@ -612,9 +630,7 @@ private int getExponentialDecay() {
612630
} else {
613631
return Math.max(position, destinationValue);
614632
}
615-
616633
}
617-
618634
private int getRubber() {
619635
if(isFinished()){
620636
return destinationValue;

CodenameOne/src/com/codename1/ui/spinner/DurationSpinner3D.java

+9-2
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,21 @@ class DurationSpinner3D extends Container implements InternalPickerWidget {
4646

4747
private Spinner3D days, hours, minutes, seconds, milliseconds;
4848
private final boolean includeDays, includeHours, includeMinutes, includeSeconds, includeMilliseconds;
49-
49+
50+
private final int minuteStep;
51+
5052
public DurationSpinner3D(int fields) {
53+
this(fields, 5);
54+
}
55+
56+
public DurationSpinner3D(int fields, int minuteStep) {
5157

5258
includeDays = (fields & FIELD_DAY) != 0;
5359
includeHours = (fields & FIELD_HOUR) != 0;
5460
includeMinutes = (fields & FIELD_MINUTE) != 0;
5561
includeSeconds = (fields & FIELD_SECOND) != 0;
5662
includeMilliseconds = (fields & FIELD_MILLISECOND) != 0;
63+
this.minuteStep = minuteStep;
5764
init();
5865
}
5966

@@ -83,7 +90,7 @@ private void init() {
8390
box.add(new Label(uim.localize("hour", "hour")));
8491
}
8592
if (includeMinutes) {
86-
minutes = Spinner3D.create(0, includeHours ? 59 : 1000, 0, 1);
93+
minutes = Spinner3D.create(0, includeHours ? 59 : 1000, 0, minuteStep);
8794
minutes.setPreferredW(new Label("000", "Spinner3DRow").getPreferredW());
8895
s = Style.createProxyStyle(minutes.getRowStyle(), minutes.getSelectedRowStyle());
8996
s.setAlignment(Component.RIGHT);

0 commit comments

Comments
 (0)