Как анимировать прокрутку в Dart?

Я хочу сделать плавную прокрутку до раздела моей страницы. В jQuery делается так:

$('html, body').animate({
    scrollTop: $('#anchorOfMyTargetSection').offset().top
}, 'slow');

в Dart я могу сделать свиток, но я понятия не имею, как его оживить:

int targetSection = querySelector('#anchorOfMyTargetSection').offsetTop;
window.scrollTo( 0, targetSection );

чтобы быть более конкретным, я делаю одну страницу со ссылками на разделы на собственной странице.

вот ссылки:

<nav>
    <ul class="nav-list">
        <li><a href="http://example.com/#title">Home</a></li>
        <li><a href="http://example.com/#about">About us</a></li>
        <li><a href="http://example.com/#products">Products</a></li>
        <li><a href="http://example.com/#services">Services</a></li>
        <li><a href="http://example.com/#contact">Contact</a></li>
    </ul>
</nav>

вот полный код jQuery:

// Generated by LiveScript 1.2.0
var smoothScrolling;
smoothScrolling = function(selector, offset){
  return $(selector).click(function(click){
    var link, anchor;
    click.preventDefault();
    link = $(this).attr('href');
    anchor = link.substring(link.indexOf('#'));
    return $('html ,body').animate({
      scrollTop: $(anchor).offset().top + offset
    }, 'slow');
  });
};
smoothScrolling('.nav-list a', -45);

и здесь код дротика (без анимационной части конечно):

import 'dart:html';

void main() {
  smoothScrollingOffset = -45;
  querySelectorAll('.nav-list a').onClick.listen(smoothScrolling);
}

int smoothScrollingOffset = 0;

void smoothScrolling(MouseEvent click) {
  click.preventDefault();
  String link = click.target.toString();
  String anchor = link.substring( link.indexOf('#') );
  int targetPosition = querySelector('$anchor').offsetTop;

  window.scrollTo( 0 , targetPosition + smoothScrollingOffset );
}

имейте в виду, что я новичок в Dart и новичок в программировании в целом. Если вы видите что-то, что может быть улучшено в коде дротика, скажите это тоже.

3 ответов


обновление: использование только библиотеки Dart: html.

вот ссылка

<nav>
    <ul class="nav-list">
        <li><a href="http://example.com/#title">Home</a></li>
        <li><a href="http://example.com/#about">About us</a></li>
        <li><a href="http://example.com/#products">Products</a></li>
        <li><a href="http://example.com/#services">Services</a></li>
        <li><a href="http://example.com/#contact">Contact</a></li>
    </ul>
</nav>

в коде Dart импортируйте библиотеку Dart: html для доступа к DOM.

import 'dart:html';

вот функция, и вот что она делает:

  • при нажатии на ссылку, это анализируется и якорь извлекается
  • он использует якорь, чтобы знать, какой элемент / раздел, чем вы ищете
  • тогда вам положение элемента / секции
  • прокрутите до элемента / раздела

.

void smoothScrolling(String selector, {int offset: 0, int duration: 500}) {

  // The detection of the clicks and the selectors of the links are specified at the bottom of the function
  // Let's suppose you click a link to visit 'http://example.com/#contact'
  // When you click the link
  void trigger(MouseEvent click) {
    // Prevent to visit the resources, like normally does
    click.preventDefault();
    // Get the resource link of the clicked element. In this case: 'http://example.com/#contact'
    String link = click.target.toString();
    // Extract the anchor. In this case from 'http://example.com/#contact' will extract '#contact'
    String anchor = link.substring( link.indexOf('#') );
    // With the extracted anchor, search the corresponding element and get his position.
    // In this case gets the position of the element with 'contact' in the 'id' attribute 
    int targetPosition = querySelector('$anchor').offsetTop;

    // Before to translate to the element,
    // you can specify if you want to translate some distance before or after the element
    targetPosition += offset;

    // Let's move in direction to the section
    // We know than in Dart there are 60 frames per second, that means a frame duration is 
    // 1000 milliseconds divided in 60 frames = per frame is 16.66 milliseconds long.
    // But 16.66 milliseconds multiplied by 60 = 999.99 milliseconds. That is ALMOST a second.
    // And that means than there will be 59.99 frames in 999.99 milliseconds
    // But we cannot handle frame fractions, we should round frames to integers
    // So 59.99 frames will be rounded to 60 frames

    // Calculate the total number of frames
    int totalFrames = ( duration / (1000 / 60) ).round();
    // The first animation frame will be the number 1, the number 0 will be the start point
    int currentFrame = 0;
    // In this case the start point will be the current position
    int currentPosition = window.scrollY;
    // The end point will be the target position, we should know how many distance there is between the start and end point.
    // The positive and negative numbers represents the same distance, that means than 'y' and '-y' are the same.
    // Example: 10 and -10 are the same distance.

    // Calculate the distance between the start and end point.
    int distanceBetween =  targetPosition - currentPosition;
    // Then calculate how many distance should move per frame
    num distancePerFrame = distanceBetween / totalFrames;

    // The animation function is triggered by first time more later in the code
    // And when is triggered
    void animation(num frame) { 
      // First we look the number of the frame we are going to run.
      // When all the frames are complete the animation function will not be executed again
      if ( totalFrames >= currentFrame ) {

        // In every frame we are going to move some distance with direction to the target.
        // The direction (in this case will be only up or down) depends of the 'distanceBetween' number.
        // Let's explore this part with an example: You are 10 pixels from your target, your target is at the point 20,
        // Remember, to calculate the distance between you and the target we do 'targetPosition - currentPosition'.
        // If you are 10 pixels on from the target (at the point 10) the result will be: 20 - 10 = 10.
        // If you are 10 pixels down from the target (at the point 30) the result will be: 20 - 30 = -10.
        // You see how the number is the same but with different polarity?. And 10 and - 10 represent the same distance
        // The direction depends of the number 0, if you move closer to the 0 you will go down, if you move away the 0 you will move up.
        // Let's move 5 pixels:
        // 10 + 5 = 15. You will move down, because you will be more away of the 0, your target is at the number 20. 
        // -10 + 5 = -5. You will move up, because you will be more closer to the 0, your target is at the number 0.

        // Let's move to the point where we should be in this frame
        window.scrollTo( 0, currentPosition );
        // Calculate the point where we should be in the next frame
        currentPosition += distancePerFrame;

        // We get ready to execute the next frame
        currentFrame++;
        // When the time of this frame (16.66 milliseconds) is complete immediately starts the next frame.
        window.animationFrame.then(animation);      
      }
    } 

    // Here is triggered the animation by first time
    window.animationFrame.then(animation);  
  }

  // Here are the links' selectors and the detection of the clicks
  querySelectorAll(selector).onClick.listen(trigger);
}

чтобы использовать функцию, мы должны указать селекторы ссылок и, возможно, смещение и / или продолжительность прокрутки.

void main() {
  // To use the function we should specify the links' selectors,
  // and optionally an offset and/or the duration of the scrolling,
  smoothScrolling('.nav-list a',
                  offset: -45,
                  duration: 2500);
}

анимация

я объясню основные моменты, все детали прокомментировал в коде.

другое дело, чтобы отметить, чем я использовал блендер длительное время, с открытым исходным кодом 3D анимационное программное обеспечение, благодаря Blender я вошел в мир программирования. Так что я знаю, о чем говорю.

1. Дротик

вот первая трудность. Если вы ищете в Dart docs что-то для обработки анимации вы найдете animationFrame, кажется, все в порядке.

но сначала, что такое рамка? Возможно, вы слышали что-то вроде 24 кадров в секунду. Это означает, что в этом случае кадр составляет 1 секунду деление на 24 = на кадр составляет 0,0416 секунды = кадр за 41,66 миллисекунды.

тогда давайте обрабатывать кадры как блоки времени, и эти блоки времени обычно обрабатываются как целое число. Мы обычно говорим что-то вроде этого: этот экран 24 кадра в секунду, но не что-то вроде этого экрана 24.6 кадров в секунду.

но есть проблема в документах.

будущее завершается меткой времени, представляющей плавающую точку значение количество миллисекунд, прошедших со страницы начал загружаться (который также является меткой времени при этом вызове animationFrame).

хорошо, это означает, что я могу знать, сколько раз пользователь был на странице, но нет никакой ссылки на то, сколько раз в секунду я могу это знать, и это то, что я ожидаю знать, когда вы говорите о кадрах: сколько кадров есть в секунду?

2. Кадров в Dart

после некоторых экспериментов Я обнаружил, что в Dart есть 60 кадров в секунду

1000 миллисекунд разделенных в 60 кадрах = в рамку 16.66 миллисекундах долго. Но 16.66 МС умножить на 60 = 999.99 миллисекунды. Это почти секунда. А это значит, чем там быть 59.99 кадров в 999.99 миллисекунд, но мы не можем обрабатывать кадр дроби, мы должны округлить фреймы до целых чисел. Так 59.99 кадры будут округляется до 60 кадров

помните a длина кадра 16,66 миллисекунды.

3. Анимация в Dart

// Set the duration of your animation in milliseconds
int duration = 1000;
// Calculate the total number of frames
int totalFrames = ( duration / (1000 / 60) ).round();
// The first animation frame will be the number 1, the number 0 will be the start point
int currentFrame = 0;

// The animation function is triggered by first time more later in the code
// And when is triggered
void animation(num frame) { 
  // First we look the number of the frame we are going to run.
  // When all the frames are complete the animation function will not be executed again
  if ( totalFrames >= currentFrame ) {

    // =========================================== 
    // Here what we are going to do in every frame 
    // ===========================================        

    // We get ready to execute the next frame
    currentFrame++;
    // When the time of this frame (16.66 milliseconds) is complete immediately starts the next frame.
    window.animationFrame.then(animation);      
  }
} 

// Here is triggered the animation by first time
window.animationFrame.then(animation);

Примечание:

1. Я нашел ошибку, написав функцию smoothScrolling, но не волнуйтесь, влияет только на виртуальную машину Dart, скомпилированный JavaScript работает так, как ожидалось. Я пытаюсь найти, что именно вызывает ошибку, и поэтому могу сообщить об этом.

то, что я нашел, чем использование номера distancePerFrame в анимации функция, чем функция не петли.

2. технически это не "плавная прокрутка", это "линейная прокрутка".


вы можете использовать пакет анимация выполнять работу

вот пример использования:

import 'dart:html';
import 'package:animation/animation.dart');

main() {
  var el = query('#box');

  var properties = {
    'left': 1000,
    'top': 350
  };

  animate(el, properties: properties, duration: 5000);
}

обновление: окончательная версия с таймингами и вспомогательной функцией для нормализации продолжительности анимации прокрутки.
должен быть протестирован во всех целевых браузерах

import 'dart:html';
import 'dart:async';
import 'dart:math';

void main() {
  Element a = querySelector("#a"),
      b = querySelector("#b"),
      c = querySelector("#c");

  document.onClick.first
      .then((_) => scrollTo(c, getDuration(c, 2), TimingFunctions.easeInOut))
      .then((_) => scrollTo(a, getDuration(a, 2), TimingFunctions.easeOut))
      .then((_) => scrollTo(c, getDuration(c, 5), TimingFunctions.easeOut))
      .then((_) => scrollTo(b, getDuration(b, 2), TimingFunctions.easeOut))
      .catchError(print);
}

double fullOffsetTop(Element element) =>
  element.getBoundingClientRect().top +
    window.pageYOffset -
    document.documentElement.clientTop;

Duration getDuration(Element targetElement, num speed){
    var distance = (window.pageYOffset - fullOffsetTop(targetElement)).abs();
    return new Duration(milliseconds: distance ~/ speed);
    }

Future scrollTo(Element el, Duration duration, TimingFunction tf) {

  var isCompleted = false,
      isInterrupted = false,
      completer = new Completer(),
      startPos = window.pageYOffset,
      targetPos = fullOffsetTop(el),
      overScroll =
      max(targetPos + window.innerHeight - document.body.scrollHeight, 0),
      startTime = null,
      direction = (targetPos - startPos).sign;

  targetPos -= overScroll;

  var totalDistance = (targetPos - startPos).abs();

  //make text unselectable and disable events
  //like onMouseOver for better performance during the scroll.
  String disable =
      "-webkit-user-select: none;"
      "-moz-user-select: none;"
      "-ms-user-select: none;"
      "-o-user-select: none;"
      "user-select: none;"
      "pointer-events: none;";

  String oldBodyStyle = document.body.getAttribute("style") != null ?
    document.body.getAttribute("style") : "";

  //return control to the user if he/she tries to interact with the page.
  window.onMouseWheel.first.then((_) => isInterrupted = isCompleted = true);
  window.onKeyDown.first.then((_) => isInterrupted = isCompleted = true);

  document.body.setAttribute("style", disable + oldBodyStyle);

  iter() {
    window.animationFrame.then((_) {

      if (startTime == null) startTime = window.performance.now();
      var deltaTime = window.performance.now() - startTime,
           progress = deltaTime / duration.inMilliseconds,
           precision = (1000 / 60 / duration.inMilliseconds) / 4,
           dist = totalDistance * tf(progress, precision);
      var curPos = startPos + dist * direction;

      if (progress >= 1.0) isCompleted = true;

      if (!isCompleted) {
        window.scrollTo(0, curPos.toInt());
        iter();
      } else {
        document.body.setAttribute("style", document.body.getAttribute("style"
            ).replaceFirst(disable, ""));
        isInterrupted ? completer.completeError("Interrupted by the user") :
            completer.complete("completed");
      }
    });
  }
  iter();
  return completer.future;
}

typedef num TimingFunction(num time,num precision);

abstract class TimingFunctions{
  static TimingFunction easeInOut = makeCubicBezier(0.42, 0, 0.58, 1);
  static TimingFunction easeOut = makeCubicBezier(0.25, 0.1, 0.25, 1);
}

TimingFunction makeCubicBezier(x1, y1, x2, y2) {
  var curveX = (t) {
    var v = 1 - t;
    return 3 * v * v * t * x1 + 3 * v * t * t * x2 + t * t * t;
  };

  var curveY = (t) {
    var v = 1 - t;
    return 3 * v * v * t * y1 + 3 * v * t * t * y2 + t * t * t;
  };

  var derivativeCurveX = (t) {
    var v = 1 - t;
    return 3 * (2 * (t - 1) * t + v * v) * x1 + 3 * (-t * t * t + 2 * v * t) *
        x2;
  };
  return (t, precision) {
    var x = t,t0,t1,t2,x2,d2,i;
    for (i = 0; i < 8; i++) {
      t2 = x;
      x2 = curveX(t2) - x;
      if (x2.abs() < precision) return curveY(t2);
      d2 = derivativeCurveX(t2);
      if (d2.abs() < 1e-6) break;
      t2 = t2 - x2 / d2;
    }
    t0 = 0;
    t1 = 1;
    t2 = x;

    if (t2 < t0) return curveY(t0);
    if (t2 > t1) return curveY(t1);
    while (t0 < t1) {
      x2 = curveX(t2);
      if ((x2 - x).abs() < precision) return curveY(t2);
      if (x > x2) {
        t0 = t2;
      } else {
        t1 = t2;
      }
      t2 = (t1 - t0) * .5 + t0;
    }
    return curveY(t2);
  };
}

https://www.youtube.com/watch?v=IyHb0SJms6w - объясняет, как pointer-events: none; помогает. https://www.youtube.com/watch?v=hAzhayTnhEI - Почему вы должны использовать AnimationFrame вместо таймера.

было бы еще лучше, если бы вы могли использовать веб Анимации, но, я думаю, это невозможно с эффектами прокрутки, потому что это не свойство css.