在可滚动容器中缩放居中的div

24

我正在尝试对一个居中于可滚动容器div的div进行比例变换。

我采用的技巧是使用包装器(wrapper),并将新的宽度/高度设置为包装器,以便父元素可以正确地显示滚动条。

.container {
    position: relative;
    border: 3px solid red;
    width: 600px; height: 400px;
    background-color: blue;
    overflow-x: scroll; overflow-y: scroll;
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: center;
}
.wrapper {
    order: 1;
    background-color: yellow;
}
.content-outer {
    width: 300px;
    height: 200px;
    transform-origin: 50% 50%;
    /*transform-origin: 0 0;*/
}
.content-outer.animatted {
    animation: scaleAnimation 1s ease-in forwards;
}
.content-outer.animatted2 {
    animation: scaleAnimation2 1s ease-in forwards;
}
.content-inner {
    width: 300px;
    height: 200px;
    background: linear-gradient(to right, red, white);
}

如果转换原点是 0,0,则该 div 将居中,但没有动画跳动,但滚动条不正确。如果原点在中间,则 div 的位置和滚动条都会错乱。

我尝试了两种居中的方法,一种是使用 flexbox (http://jsfiddle.net/r3jqyjLz/1/),另一种是使用负边距 (http://jsfiddle.net/roLf5tph/1/)。

有更好的方法吗?


你不能这样做。在我遇到的所有浏览器中,左侧和顶部(如果 direction:rtl 则为右侧)都是不可滚动的区域。这通常是隐藏元素使用 position: absolute; left: -999999px; 技巧的剩余影响。你可以使用几个第三方工具来自定义滚动条,但老实说,我会质疑导致这种情况的设计。 - Josh Burgess
5个回答

8
我假设你遇到的问题可以总结如下:
  1. 您在内部 div 中有一个复杂的结构,希望将其作为一组进行缩放。 (如果它是单个元素,则使用矩阵很容易解决)。
  2. 当内部 div 缩放超出容器范围时,如果不控制宽度/高度,则无法获得滚动条。
  3. 您希望内部 div 保持居中,并且同时滚动条应反映正确位置。

对此,有两个(实际上是三个)简单的选项。

选项1:

使用与问题中相同的标记,当比例因子低于1时,您可以使 div 保持居中。对于比例因子大于1的情况,您将其更改为左上角。容器上的 overflow: auto 将自行处理滚动,因为通过变换缩放的 div 包装在另一个 div 中。您实际上不需要Javascript。

这解决了您的问题1和2。

小工具 1: http://jsfiddle.net/abhitalks/c0okhznc/

代码片段 1:

* { box-sizing: border-box; }
#container {
    position: relative;
    border: 1px solid red; background-color: blue;
    width: 400px; height: 300px;
    overflow: auto;
}
.wrapper {
    position: absolute; 
    background-color: transparent; border: 2px solid black;
    width: 300px; height: 200px;
    top: 50%; left: 50%;
    transform-origin: top left;
    transform: translate(-50%, -50%); 
    transition: all 1s;
}
.box {
    width: 300px; height: 200px;
    background: linear-gradient(to right, red, white);
    border: 2px solid yellow;
}
.inner { width:50px; height: 50px; background-color: green; }

#s0:checked ~ div#container .wrapper { transform: scale(0.5) translate(-50%, -50%); }
#s1:checked ~ div#container .wrapper { transform: scale(0.75) translate(-50%, -50%); }
#s2:checked ~ div#container .wrapper { transform: scale(1) translate(-50%, -50%); }
#s3:checked ~ div#container .wrapper { top: 0%; left: 0%; transform-origin: top left; transform: scale(1.5) translate(0%, 0%); }
#s4:checked ~ div#container .wrapper { top: 0%; left: 0%; transform-origin: top left; transform: scale(2) ; }
#s5:checked ~ div#container .wrapper { top: 0%; left: 0%; transform-origin: top left; transform: scale(3) ; }
<input id="s0" name="scale" data-scale="0.5" type="radio" />Scale 0.5
<input id="s1" name="scale" data-scale="0.75" type="radio" />Scale 0.75
<input id="s2" name="scale" data-scale="1" type="radio" checked />Scale 1
<input id="s3" name="scale" data-scale="1.5" type="radio" />Scale 1.5
<input id="s4" name="scale" data-scale="2" type="radio" />Scale 2
<input id="s5" name="scale" data-scale="3" type="radio" />Scale 3
<hr>
<div id="container">
    <div id="wrapper" class="wrapper">
        <div id="box" class="box">
            <div class="inner"></div>
        </div>
    </div>
</div>

但是这会带来另一个问题。当
被缩放超出容器时,overflow:auto将导致跳跃/闪烁。这可以通过使其始终显示滚动条(就像您已经做的那样)来轻松解决。尽管在Firefox中它能够完美运行,但Chrome在此处表现不佳,并且无法更新滚动条位置。这里的诀窍是使用Javascript强制重新流动,通过在缩放完成后将overflow更改为auto。所以你需要稍微延迟一下。

Fiddle 2: http://jsfiddle.net/abhitalks/u7sfef0b/

Snippet 2

$("input").on("click", function() {
    var scroll = 'scroll', 
        scale = +($(this).data("scale"));
    if (scale > 1) { scroll = 'auto'; }
    setTimeout(fixScroll, 300, scroll);
});

function fixScroll(scroll) { $("#container").css({ overflow: scroll }); }
* { box-sizing: border-box; }
#container {
    position: relative;
    border: 1px solid red; background-color: blue;
    width: 400px; height: 300px;
    overflow: scroll;
}
.wrapper {
    position: absolute; 
    background-color: transparent; border: 2px solid black;
    width: 300px; height: 200px;
    top: 50%; left: 50%;
    transform-origin: top left;
    transform: translate(-50%, -50%); 
    transition: all 1s;
}
.box {
    width: 300px; height: 200px;
    background: linear-gradient(to right, red, white);
    border: 2px solid yellow;
}
.inner { width:50px; height: 50px; background-color: green; }

#s0:checked ~ div#container .wrapper { transform: scale(0.5) translate(-50%, -50%); }
#s1:checked ~ div#container .wrapper { transform: scale(0.75) translate(-50%, -50%); }
#s2:checked ~ div#container .wrapper { transform: scale(1) translate(-50%, -50%); }
#s3:checked ~ div#container .wrapper { top: 0%; left: 0%;transform-origin: top left; transform: scale(1.5) translate(0%, 0%); }
#s4:checked ~ div#container .wrapper { top: 0%; left: 0%;transform-origin: top left; transform: scale(2) ; }
#s5:checked ~ div#container .wrapper { top: 0%; left: 0%;transform-origin: top left; transform: scale(3) ; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<input id="s0" name="scale" data-scale="0.5" type="radio" />Scale 0.5
<input id="s1" name="scale" data-scale="0.75" type="radio" />Scale 0.75
<input id="s2" name="scale" data-scale="1" type="radio" checked />Scale 1
<input id="s3" name="scale" data-scale="1.5" type="radio" />Scale 1.5
<input id="s4" name="scale" data-scale="2" type="radio" />Scale 2
<input id="s5" name="scale" data-scale="3" type="radio" />Scale 3
<hr>
<div id="container">
    <div id="wrapper" class="wrapper">
        <div id="box" class="box">
            <div class="inner"></div>
        </div>
    </div>
</div>

选项2:

为了解决问题3,即保持

居中并同时保持正确的滚动条位置,您需要回退到Javascript。

原则仍然相同,对于比例因子大于1的情况,您需要重置左上角和translate位置。您还需要重新计算缩放后的宽度/高度,然后将其重新分配给包装器

。然后,在容器上设置scrollTopscrollLeft将变得非常容易,只需获取包装器
和容器
之间的差异即可。

这解决了您的问题1、2和3。

Fiddle 3: http://jsfiddle.net/abhitalks/rheo6o7p/1/

Snippet 3:

var $container = $("#container"), 
    $wrap = $("#wrapper"), 
    $elem = $("#box"), 
    originalWidth = 300, originalHeight = 200;

$("input").on("click", function() {
    var factor = +(this.value);
    scaler(factor);
});

function scaler(factor) {
    var newWidth = originalWidth * factor,
        newHeight = originalHeight * factor;
    $wrap.width(newWidth); $wrap.height(newHeight);
    if (factor > 1) {
        $wrap.css({ left: 0, top: 0, transform: 'translate(0,0)' }); 
        $elem.css({ transform: 'scale(' + factor + ')' }); 
        setTimeout(setScroll, 400);
    } else {
        $elem.css({ transform: 'scale(' + factor + ') ' });        
        $wrap.css({ left: '50%', top: '50%', transform: 'translate(-50%, -50%)' });
    }
}

function setScroll() {
    var horizontal, vertical;
    horizontal = ($wrap.width() - $container.width()) / 2;
    vertical = ($wrap.height() - $container.height()) / 2;
    $container.stop().animate({scrollTop: vertical, scrollLeft: horizontal}, 500);
}
* { box-sizing: border-box; }
#container {
    position: relative;
    border: 1px solid red; background-color: blue;
    width: 400px; height: 300px;
    overflow: scroll;
}
.wrapper {
    position: absolute; 
    background-color: transparent; border: 2px solid black;
    width: 300px; height: 200px;
    top: 50%; left: 50%;
    transform: translate(-50%, -50%); 
    transition: all 0.5s;
}
.box {
    width: 300px; height: 200px;
    background: linear-gradient(to right, red, white);
    border: 2px solid yellow;
    transform-origin: top left;
    transition: all 0.5s;
}
.inner { width:50px; height: 50px; background-color: green; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<label for="slider1">Scale: </label>
<input id="slider1" type="range" min="0.5" max="3" value="1" step="0.25" list="datalist" onchange="scaleValue.value=value" />
<output for="slider1" id="scaleValue">1</output>
<datalist id="datalist">
 <option>0.5</option>
 <option>0.75</option>
 <option>1</option>
    <option>1.5</option>
    <option>2</option>
    <option>3</option>
</datalist>
<hr>
<div id="container">
    <div id="wrapper" class="wrapper">
        <div id="box" class="box">
            <div class="inner"></div>
        </div>
    </div>
</div>

所有脚本均已在IE-11、GC-43和FF-38上进行测试

.


我成功解决了这个问题,方法是将容器 div 与缩放 div 分离开来。这样做还能满足另一个目标,即缩放到某一点(当前所有解决方案中的缩放动画都很奇怪)。 不过你也很接近了。感谢你清晰的总结,赏金归你了。 - Ahmed Kotb
不想打击你的兴致,但是对我来说,这些解决方案都无法平滑滚动。 - som
@AhmedKotb(或其他任何人):我无法从您的评论中确定,但您是否找到了一种方法,使动画在比例>1时不会首先缩放到左上角,然后再通过动画滚动重新定位? - Adam Prescott
@AdamPrescott 我通过将两个div完全分开来使其工作,我们称之为缩放div和可滚动div,第一个div使用负边距居中,我手动进行所有所需的数学计算以进行缩放/动画,动画结束后,我将缩放div的宽度/高度复制到可滚动div内的虚拟div中,这给我提供了本机滚动条。同时,我还需要处理onscroll以实际在缩放div上执行translateX/Y。这有意义吗? - Ahmed Kotb

8

这符合你的要求吗

我使用了CSS过渡来实现缩放动画。

#centered {
    background-image: linear-gradient(to bottom,yellow,red);
    height: 200px;
    transform: scale(1);
    transition: transform 1s ease-out;
    width: 200px;
}

#scrollable {
    align-items: center;
    border: 1px solid red;
    display: flex;
    height: 300px;
    justify-content: center;
    overflow: auto;
    width: 300px;
}

更新 #1

这个怎么样?

  • 使用传统的绝对居中方式(绝对定位)
  • 我明白了你的观点关于 flexbox 。当被居中的元素比容器大时,它似乎有一些限制。

这个与之前相同的问题,滚动不正常(https://jsfiddle.net/6e2g6vzt/2/)。我已经用一张相同大小的随机图片替换了渐变色,以便更明显地显示滚动问题。(尝试缩放3倍,图片从顶部裁剪,人物面部不可见 :)) - Ahmed Kotb
更新后的解决方案仍存在同样的问题,https://dl.dropboxusercontent.com/u/8045913/temp/problem.PNG。我已经尝试在问题的第二个链接中使用负边距。 - Ahmed Kotb

3
这个问题的难点在于,在缩放过程中,你需要从一个模型(你仍然有边距)切换到另一个模型,在这个模型中,你需要扩大容器的大小。
由于这发生在变换的中间,很难通过单个属性变换来处理。
我找到的解决方法是改变min-height和min-width属性。这将使得自动检测这一点并优雅地处理它成为可能。
我只保留最基本的功能在JS中,其他所有功能都在CSS中完成。

function setZoom3() {
    var ele = document.getElementById("base");
    ele.className = "zoom3";
}

function setZoom1() {
    var ele = document.getElementById("base");
    ele.className = "";
}

function setZoom05() {
    var ele = document.getElementById("base");
    ele.className = "zoom05";
}
.container {
    width: 300px;
    height: 300px;
    overflow: scroll;
    position: relative;
}

#base {
    width: 300px;
    height: 300px;
    top: 0px;
    left: 0px;
    position: absolute;
    min-width: 300px;
    min-height: 300px;
    transition: min-width 5s, min-height 5s;
}

.inner {
    width: 200px;
    height: 200px;
    background-color: lightgreen;
    background-image: linear-gradient(-45deg, red 5%, yellow 5%, green 95%, blue 95%);
    top: 50%;
    left: 50%;
    position: absolute;
    transform: translate(-50%, -50%); 
    transform-origin: top left;
    transition: transform 5s;
}

#base.zoom3 {
    min-width: 600px;
    min-height: 600px;
}
.zoom3 .inner {
    transform: scale(3) translate(-50%, -50%);
}

.zoom05 .inner {
    transform: scale(0.5) translate(-50%, -50%);
}

.trick {
    width: 20px;
    height: 20px;
    background-color: red;
    position: absolute;
    
    transform: translate(0px);
}
<div class="container">
    <div id="base">
        <div class="inner"></div>
    </div>
</div>
<button onclick="setZoom3();">zoom 3</button>
<button onclick="setZoom1();">zoom 1</button>
<button onclick="setZoom05();">zoom 0.5</button>

唯一剩下的问题是设置滚动位置,以防您希望它保持在中心。

2
我用一点逻辑解决了这个问题。我使用了GSAP库进行动画和更新,但你也可以使用纯JS轻松实现:

http://codepen.io/theprojectsomething/pen/JdZWLV

... 由于需要滚动父级div以保持元素居中(无法通过动画滚动),因此仅使用CSS无法实现平滑过渡。

注意:即使没有滚动,纯CSS过渡似乎也有同步问题(偏移量,无论是top / left,margin还是translate,都在缩放时一直调整),可能是由于亚像素定位导致的?其他人可能能够提供进一步的见解。

Codepen的完整代码:

var $inner = document.querySelector('aside'),
    $outer = document.querySelector('main'),
    $anim = document.querySelector('[type="checkbox"]'),
    $range = document.querySelector('[type="range"]'),
    data = {
      width: $outer.clientWidth,
      value: 1
    };

$range.addEventListener('input', slide, false);

function slide () {
  $anim.checked ? animate(this.value) : transform(this.value);
}

function animate (value) {
  TweenLite.to(data, 0.4, {
    value: value,
    onUpdate: transform
  });
}

function transform (value) {
  if( !isNaN(value) ) data.value = value;
  
  var val = Math.sqrt(data.value),
      offset = Math.max(1 - val, 0)*0.5,
      scroll = (val - 1)*data.width*0.5;

  TweenLite.set($inner, {
    scale: val,
    x: offset*100 + "%",
    y: offset*100 + "%"
  });
  
  TweenLite.set($outer, {
    scrollLeft: scroll,
    scrollTop: scroll
  });
}

window.onresize = function (){
  data.width = $outer.clientWidth;
  transform(data.value);
};
main, aside:before, footer {
  position: absolute;
  top: 50%;
  left: 50%;
  -webkit-transform: translate(-50%, -50%);
      -ms-transform: translate(-50%, -50%);
          transform: translate(-50%, -50%);
}

main {
  width: 50vh;
  height: 50vh;
  min-width: 190px;
  min-height: 190px;
  background: black;
  overflow: scroll;
  box-sizing: border-box;
}

aside {
  position: relative;
  width: 100%;
  height: 100%;
  border: 1vh solid rgba(0, 0, 0, 0.5);
  background-image: -webkit-linear-gradient(top, yellow, red);
  background-image: linear-gradient(to bottom, yellow, red);
  box-sizing: border-box;
  -webkit-transform-origin: 0 0;
      -ms-transform-origin: 0 0;
          transform-origin: 0 0;
}
aside:before {
  content: "";
  background: black;
  border-radius: 50%;
  width: 10%;
  height: 10%;
  display: block;
  opacity: 0.5;
}

input {
  display: block;
  margin: 3em auto;
}
input:after {
  content: attr(name);
  color: white;
  text-transform: uppercase;
  position: relative;
  left: 2em;
  display: block;
  line-height: 0.9em;
}
<script src="//cdnjs.cloudflare.com/ajax/libs/gsap/1.16.1/TweenMax.min.js"></script>
<main>
  <aside></aside>
</main>
<footer>
  <input type="checkbox" name="animate" checked>
  <input type="range" min="0.1" step="0.005" max="3">
</footer>


1
Matthew King的小提琴已经接近正确了。唯一需要修正的是div的偏移量。如果你的内部div超出了可滚动div的边界(负偏移),你需要将偏移量设置为0。否则,你希望div居中显示。然而,居中并不那么简单。你正在使用css缩放div的背景,因此你没有影响到div的实际宽度和高度,需要解析比例矩阵来计算其背景的尺寸。

这个解决方案对我有用:https://jsfiddle.net/6e2g6vzt/11/(至少在Ubuntu上的FF上有效,还没有在其他浏览器上测试过)。基本上只需要一个函数来设置新的偏移量,你甚至不需要手动调用它,因为这行代码会自动调用它。
$("#centered").bind("transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd", setNewOffset);

在变换完成调用setNewOffset函数。
正如你所看到的,图像在变换后会“跳”到其正确的位置,也许你想添加一个平滑的效果来弥补这一点,但我只是想向你展示如何获得正确的偏移量。

查看jQuery文档以了解更多关于.offset()的信息。


致谢:
感谢Jim Jeffers在转换后的回调方面提供的帮助
https://dev59.com/TWox5IYBdhLWcg3wWzJ7#9255507
感谢Lea Verou为解析比例矩阵提供的优秀正则表达式
https://dev59.com/p2035IYBdhLWcg3wJcYv#5604199


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接