使用水平滚动处理 React 动画

Posted

技术标签:

【中文标题】使用水平滚动处理 React 动画【英文标题】:Handling React animation with horizontal scrolling 【发布时间】:2015-03-09 09:19:14 【问题描述】:

我正在尝试使用 React 和 React.addons.CSSTransitionGroup 创建一个带有动画的水平滚动页面导航。目前我能够进行水平滚动(使用 flexbox)、页面打开/关闭、动画进入和离开。但动画并不是我想要的。

看看this example (jsfiddle)。

当您单击前一页上的按钮时,它当前会将离开屏幕的页面推到右侧。尽管正确的结果是在同一位置为离开页面设置动画。我不确定如何使用 CSSTransitionGroup 来实现这种效果,也不知道它是否可行。

function generateId() 
  var r = "";
  var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  for (var i = 0; i < 10; i += 1) 
    r += possible.charAt(Math.floor(Math.random() * possible.length));
  
  return r;


var PageList = 
  pages: [],
  listener: function(newpages) ,
  open: function(caption, index) 
    if (index != null) 
      this.pages = this.pages.slice(0, index + 1);
    
    this.pages.push(
      id: generateId(),
      caption: caption,
      width: (150 + Math.random() * 50) | 0
    );
    this.listener(this.pages);
  
;
PageList.open("Main");

var PageLink = React.createClass(
  render: function() 
    var self = this;
    return React.DOM.button(
      className: "pagelink",
      onClick: function() 
        PageList.open(self.props.caption, self.props.pageIndex);
      
    , self.props.caption);
  
);

var Page = React.createClass(
  render: function() 
    return React.DOM.article(
        className: "page",
        style: 
          width: this.props.page.width + "px"
        
      ,
      React.DOM.h1(, this.props.page.caption),
      React.createElement(PageLink, 
        caption: "Alpha",
        pageIndex: this.props.index
      ),
      React.createElement(PageLink, 
        caption: "Beta",
        pageIndex: this.props.index
      ),
      React.createElement(PageLink, 
        caption: "Gamma",
        pageIndex: this.props.index
      )
    );
  
);

var Pages = React.createClass(
  componentDidMount: function() 
    var self = this;
    PageList.listener = function(pages) 
      self.setState(
        pages: pages
      );
    ;
  ,
  getInitialState: function() 
    return 
      pages: PageList.pages
    ;
  ,
  render: function() 
    var pages = this.state.pages.map(function(page, index) 
      return React.createElement(Page, 
        key: page.id,
        index: index,
        page: page
      );
    );
    return React.createElement(
      React.addons.CSSTransitionGroup, 
        component: "section",
        className: "pages",
        transitionName: "fall"
      , pages);
  
);

React.initializeTouchEvents(true);
React.render(
  React.createElement(Pages),
  document.getElementById("main")
);
/* animation */

.fall-enter 
  transform: translate(0, -100%);
  transform: translate3d(0, -100%, 0);

.fall-enter.fall-enter-active 
  transform: translate(0, 0);
  transform: translate3d(0, 0, 0);
  transition: transform 1s ease-out;

.fall-leave 
  index: -1;
  transform: translate(0, 0);
  transform: translate3d(0, 0, 0);
  transition: transform 1s ease-in;

.fall-leave.fall-leave-active 
  transform: translate(0, 100%);
  transform: translate3d(0, 100%, 0);

/* other */

* 
  box-sizing: border-box;

.pagelink 
  padding: 5px;
  margin: 5px 0px;
  width: 100%;

.pages 
  display: flex;
  flex-flow: row;
  overflow-x: scroll;
  overflow-y: hidden;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: #ddd;

.pages:after 
  flex: none;
  -webkit-flex: none;
  display: block;
  content: " ";
  width: 100px;

.page 
  flex: none;
  -webkit-flex: none;
  margin: 8px;
  padding: 10px 31px;
  background: #fff;
  overflow: auto;
  box-shadow: 2px 1px 4px rgba(0, 0, 0, 0.2);
  border-radius: 3px;
<body id="main">
</body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.12.2/react-with-addons.js"></script>

PS:如果它只适用于最新的浏览器就可以了。

【问题讨论】:

您考虑过使用绝对定位吗?如果没有,您必须确保视口的高度等于在一列中包含两行元素的最大空间量。 是的,我已经考虑过了。我想在 CSS 中保留尽可能多的样式。 你一定要使用 React 吗?此外,当用户快速点击时,期望的行为是什么?是否应该在动画完成之前忽略所有额外的点击,还是应该排队? @AndyM - 我不必使用 React,但它确实解决了我更新屏幕的很多问题,而且我已经有很多依赖它的代码。当用户快速点击时,动画和页面应该重叠,最新的屏幕是最顶部的项目。基本上,用户可以操作屏幕的最长时间是进入和离开屏幕所需的时间。 你能举一个动画应该是什么样子的例子吗?我可能会想出一些东西,但不确定你希望它是什么样子。 【参考方案1】:

问题

要获得您正在寻找的页面的垂直排列,我们将不得不更改 html 以添加一个额外的层。这个额外的层将是一个垂直通道,articles 在其中上下滑动。现在,新的只是被推到一边,因为没有父层来包含它们。

第二个问题是所有的 HTML 都是由 React 动态创建的。尝试进入并更改库/插件不是一个好主意,因此我尝试使用 jQuery 创建您想要的效果。

可能的解决方案

HTML 结构

我做的第一件事就是勾勒出想要的布局。我们需要在整个父级内部三层:

Parent
    Cell
        Innercell (doubles in height to contain two articles)
            Article

草图中的虚线是Innercell,页面矩形是Articles

CSS

在大多数情况下,我使用的是您的原始样式。主要区别在于cellarticle 之间的额外层。 cell 将其高度固定为100%,然后innercell 得到height:200%。它也是绝对定位的,bottom:0。这很重要,因为这意味着额外的间距在父级上方而不是下方。我们还确保文章本身是绝对定位的,bottom:0。这会将其置于innercell 的下半部分。

JS

在这里要做的第一件事是弄清楚我们希望事情发生的顺序。单击按钮时,我们可以走三种可能的路线。

    如果点击了最右边的按钮,我们想添加一个新单元格,然后滑入一篇文章。

    如果点击了倒数第二个按钮,我们只想滑入一篇新文章。

    如果单击任何其他按钮,我们希望在右侧单元格中滑入一篇新文章,并且所有右侧单元格都应该向下滑动且没有替换。

change article圈内,我们需要:

1 从模板新建article,插入所有变量数据,放在innercell顶部的空白处。

2 我们需要向下滑动innercell

3 动画完成后,我们需要去掉之前的article,重新设置innercell

最终代码

var cellTemplate = '<cell data-col="DATACOL"><innercell><article></article></innercell></cell>';
var innerTemplate = '<article class="new"><innerarticle><h1>TITLE</h1><button>Alpha</button><button>Beta</button><button>Gamma</button></innerarticle></article>';
var numCells = 2;
var animating = 0;

$('body').on('click', 'article button', function(event)
    if (!animating) 
        var currentCell = parseInt($(this).parent().parent().parent().parent().attr('data-col'));
        var title = $(this).text();
        if (currentCell == numCells) 
            addCell(title);
         else if (currentCell == numCells - 1) 
            changeArticle(title,numCells);
         else 
            removeExtraCells(title,currentCell);
        
    
);

function removeExtraCells(title,currentCell) 
    var tempCurrentCell = currentCell;
    changeArticle(title,tempCurrentCell+1);
    while (tempCurrentCell < numCells) 
        tempCurrentCell++;
        deleteArticle(title,tempCurrentCell+1);
    
    numCells = currentCell+1;


function addCell(title)
    numCells++
    var html = cellTemplate.replace('DATACOL',numCells);
    $('section').append(html);
    changeArticle(title,numCells);


function changeArticle(title,changingCol) 
    var cell = $('cell[data-col="'+changingCol+'"] innercell');
    var html = innerTemplate.replace('TITLE', title).replace('DATACOL', numCells - 1);
    cell.prepend(html);
    triggerAnimation(cell);


function deleteArticle(title,changingCol) 
    var cell = $('cell[data-col="'+changingCol+'"] innercell');
    var html = '<article></article>';
    cell.prepend(html).addClass('deleting');
    triggerAnimation(cell);


function triggerAnimation(cell) 
    cell.addClass('animating');
    window.setTimeout(function(event)
        cell.css('bottom','-100%');
        animating = 1;
    ,50);


$('body').on('webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd transitionend', 'innercell', function(event)
    if ( $(this).hasClass('deleting') ) 
        $(this).parent().remove();
     else 
        $(this).children('article:last-child').remove();
        $(this).children('article.new').removeClass('new');
        $(this).removeClass('animating');
        $(this).css('bottom','0');
    
    animating = 0;
);
/* other */
 * 
    box-sizing: border-box;


section 
    display: flex;
    flex-flow:row;
    overflow-x: scroll;
    overflow-y: hidden;
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: #ddd;


cell 
    flex: none;
    -webkit-flex: none;
    display:block;
    height:100%;
    float:left;
    position:relative;
    width:166px;

innercell 
    display:block;
    height:200%;
    width:100%;
    position:absolute;
    left:0;
    bottom:0;

.animating 
    transition: bottom 1s ease-out;

article 
    display:block;
    padding:8px;
    height:50%;
    position:absolute;
    left:0;
    bottom:0;

article.new 
    bottom:auto;
    top:0;

innerarticle 
    display:block;
    padding: 10px 31px;
    background: #fff;
    overflow: auto;
    box-shadow: 2px 1px 4px rgba(0, 0, 0, 0.2);
    border-radius: 3px;
    height:100%;


innerarticle button 
    padding: 5px;
    margin: 5px 0px;
    width: 100%;
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<section>
    <cell data-col="1">
        <innercell>
            <article>
                <innerarticle>
                    <h1>Main</h1>
                    <button>Alpha</button>
                    <button>Beta</button>
                    <button>Gamma</button>
                </innerarticle>
            </article>
        </innercell>
    </cell>
    <cell data-col="2">
        <innercell>
            <article>
                <innerarticle>
                    <h1>Alpha</h1>
                    <button>Alpha</button>
                    <button>Beta</button>
                    <button>Gamma</button>
                </innerarticle>
            </article>
        </innercell>
    </cell>
</section>

备注

我最终添加了第四层,我称之为innerarticle。这是因为您的article 上有一个8px 边距。两个articles 垂直重叠,margin 折叠并且不堆叠。添加一个innerarticle,然后给article 一个padding:8px,我可以确保它堆叠起来。

另外,您可能已经注意到我使用了很多自定义元素名称。我这样做是因为它有效,而且它让你很容易理解正在做什么。您可以随意将它们全部更改为&lt;div class="INSERTCURRENTELEMENT"&gt;

【讨论】:

很好的答案,很好的解释!结果非常顺利。 +1 这个非常好的答案。 不错的答案。但是,用 vanilla JS 做这件事很容易,我在将那部分与 React 集成时遇到了麻烦。基本上我不太确定如何与 React 系统交互。 (同样快速点击也无济于事,即想象如果用户误点击按钮,他将不得不等到页面出现,同时用户的一些输入也可能丢失。) 我的评论回复是“我不必使用 React”。如果您仍在尝试,那么显然这个答案的 JS 部分不起作用。 HTML/CSS 部分确实如此,所以唯一的问题是,你能让 React 输出一个额外的层吗? 我接着说“......但它确实解决了我更新屏幕的很多问题,而且我已经有很多依赖它的代码。”,这意味着解决了 N 个不同的问题.我猜 React 可以有额外的层,但我还没有完全弄清楚里面是如何连接的。我猜有一些很好的方法可以在 div 进入时从左侧固定位置,让它在就位时弯曲,然后在它离开时固定左侧位置......但我不知道该怎么做正确。 我想我们可以做到!所以现在,您正在为页面创建一个类。我们只需要为调用页面的单元格创建一个新类。棘手的部分在于让按钮移动页面而不是单元格。【参考方案2】:

想出了一个简单的解决方案,将其添加到 css 中:

.fall-enter.fall-enter-active 
    position: absolute;
    height: 97%;
    margin-top: -5px;

.fall-enter.fall-enter-active, .fall-leave.fall-leave-active 
    transition: transform 1s ease-in;

工作示例: http://jsfiddle.net/6985qr33/5/

玩得开心

【讨论】:

这种方法似乎有很多小故障。尤其是当您快速点击项目时。 嗯,这是一个javascript问题,首先你需要停止按下一个按钮,告诉第一个按钮移动完成,其次当你添加一个新部分时,它需要在添加之前有css类,而不是添加后。这就是造成您看到的故障的原因。

以上是关于使用水平滚动处理 React 动画的主要内容,如果未能解决你的问题,请参考以下文章

有没有办法在水平滚动视图中对特定偏移进行动画处理?

在 React 中处理滚动动画

react-transition-group动画以及数字滚动效果实现

jQuery 水平滚动(点击和动画)

如何在颤动中制作水平滚动动画

React AgGrid 提高水平滚动性能