/** * Copyright 2010 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. * */ package org.waveprotocol.box.server.rpc.render.scroll; import com.google.common.annotations.VisibleForTesting; /** * A target-based scroller that tries to be smart by making aesthetically * pleasing viewport movements. * * @param <M> type of target entities to which this scroller can move */ public final class SmartScroller<M> implements TargetScroller<M> { /** * Defines how to move the viewport between two extents. */ enum ScrollStrategy { SMART { @Override double move(Extent from, Extent target, Extent viewport) { // Constraints: // 0. The target top must end up in the viewport. // 1. The target region must be maximally within the viewport (no // viewport shift could include more of the target). // 2. If the target is already enclosed by the viewport, there is no // movement. // 3. If it is a valid location by the other constraints, the target // must appear at the viewport location of the previously focused // target. // Otherwise, the viewport should be moved minimally to bring the target // to a valid location. // Constraint 0 determines an initial range. double minStart = target.getEnd() - viewport.getSize(); double maxStart = target.getStart(); // Target too big? if (minStart >= maxStart) { // Constraint 1 determines the answer. return maxStart; } // Already valid? if (minStart <= viewport.getStart() && viewport.getStart() <= maxStart) { // Contraint 2 determines the answer. return viewport.getStart(); } // Is previous location good? if (from != null) { double stableStart = viewport.getStart() + target.getStart() - from.getStart(); if (minStart <= stableStart && stableStart <= maxStart) { // Constraint 3 determines the answer. return stableStart; } } // Pick minimal movement. We know current viewport start is either // before the min or after the max. if (viewport.getStart() < minStart) { return minStart; } else { assert viewport.getStart() > maxStart; return maxStart; } } }, // Other strategies may turn up over time. ; /** * @param from location of the previously focused target, or {@code null} * @param target location of the new target (never {@code null}) * @param viewport location of the viewport (never {@code null}) * @return the new viewport location for a shift from previously focused * {@code from} to new target {@code to}, with current viewport * location of {@code viewport}. */ abstract double move(Extent from, Extent target, Extent viewport); } private final ScrollPanel<? super M> scroller; /** Last element brought in to view. Used for smart scrolling. */ private M previousTarget; @VisibleForTesting SmartScroller(ScrollPanel<? super M> scroller) { this.scroller = scroller; } /** * Creates a smart scroller for a scroll panel. */ public static <M> SmartScroller<M> create(ScrollPanel<? super M> panel) { return new SmartScroller<M>(panel); } @Override public void moveTo(M target) { ScrollStrategy style = ScrollStrategy.SMART; Extent from = previousTarget != null ? scroller.extentOf(previousTarget) : null; Extent to = scroller.extentOf(target); Extent viewport = scroller.getViewport(); scroller.moveTo(style.move(from, to, viewport)); previousTarget = target; } }