ionic-team / ionic-framework

A powerful cross-platform UI toolkit for building native-quality iOS, Android, and Progressive Web Apps with HTML, CSS, and JavaScript.
https://ionicframework.com
MIT License
50.92k stars 13.51k forks source link

Feature request: Coupling two ion-scrolls (or setting an element within an ion-scroll to fixed on one axis) #1462

Closed CoenWarmer closed 10 years ago

CoenWarmer commented 10 years ago

Hey there,

I'm building a Timetable screen for a festival app. I'm using ion-scroll and ion-content to scroll the x axis of the screen, so you can scroll left to right to see more events.

This works, check out: http://codepen.io/squrler/pen/xihmk6

However! In cases when there are too many rows, some events might end up below the viewport so I would now also like to make the list scrollable vertically.

As far as I know that is currently not possible with the implementations of ion-scroll that we have right now.

It would be really helpful if it were possible to couple two ion-scroll to each other, and being able to scroll one ion-scroll on the x and y axis, whilst the other ion-scroll is locked on one axis.

This would allow for very cool scrolling effects, possibly leading to very easy to deploy parallax scrolling.

ajoslin commented 10 years ago

I just pushed a fix making sure both ion-scroll and ion-content support direction x and y scrolling. It worked in my tests.

EDIT: Hold on, not pushing yet. Found a problem.

ajoslin commented 10 years ago

You can now put direction="xy" on an ion-content or ion-scroll element and it will scroll both ways.

The documentation now reflects this, too.

CoenWarmer commented 10 years ago

That's nice to have @ajoslin thanks. It's not completely what I meant though in the feature request. This issue should be reopened.

I'm talking about coupling two or more ion-scrolls to each other, and defining some kind of mechanism that would allow them to interact with each other.

One use case would be to have a spreadsheet like scenario in which you can set a row to 'always stay on top'. You could then scroll vertically and horizontally through the document, while the header bar would remain fixed at the top, but would still scroll on one axis (left to right).

Related to this would be sticky list headers for instance.

Basically ion-scroll needs a mechanism to set an element to be sticky in one place. It would be really cool if you could set that element to be sticky on one axis (so either x or y). So it would scroll on one axis, but not the other.

ajoslin commented 10 years ago

Sounds crazy! :dancer:

You could do it by catching the scroll event on your main scroll element and using that to scroll the other one, if it's the axis you want.

CoenWarmer commented 10 years ago

Yes! Exactly. Is something like that possible? Do ion-scroll's emit their scrolling in some way?

ajoslin commented 10 years ago

Yes. They emit a scroll event, check that event's detail object. This isn't documented, it should be.

element.on('scroll', function(e) { console.log(e.detail.scrollTop, e.detail.scrollLeft); });

CoenWarmer commented 10 years ago

Awesome. I'll take a look to see what I can come up with.

ajoslin commented 10 years ago

If you want to stop the user from scrolling on the fixed element with his fingers...

$ionicScrollDelegate.$getByHandle('fixedElement').getScrollView().__removeEventHandlers();

This is of course very evil, touching a private API, and will get you arrested in 27 countries.

CoenWarmer commented 10 years ago

Well, at least Zynga was good for something I guess ;)

beiliubei commented 10 years ago

In Ios UIScrollview, I have three view side-to-side setuped, and one is webview which used ion-content and ion-refresher.However, except setting "scroll='false'"in ion=content, it won't be scrolled.In other words,UIScrollview can't handle the horizontal events.And it's ok in Android.

CoenWarmer commented 10 years ago

I have no idea what you're talking about @beiliubei. Are you reporting a bug? Or contributing to the discussion about this feature request? And what is this about UIScrollview?

evandsandler commented 10 years ago

@CoenWarmer, were you able to successfully couple multiple ion-scrolls in the way you described? I would be interested in the solution as I am trying to do something similar. Also, I noticed the solution you describe may help solve this issue on the forum: http://forum.ionicframework.com/t/when-using-horizontal-scroll-ion-scroll-page-cant-be-scrolled-down-in-y-direction/3833

CoenWarmer commented 10 years ago

Hey @evandsandler, I haven't made much progress on this I'm afraid. About the solution on the forum: that's interesting- I hadn't considered doing it that way. Thanks for bringing it to my attention!

I'll try to see if this would work in my scenario. Drawback is, as they mentioned in the thread on the forum that the scroll position on the x axis won't be remembered this way- makes sense since it's just an overflowed div, and thus falls outside of the more advanced functionality that ion-scroll / ion-content offer.

I still think an implementation where you can 'couple' two or more ion-scrolls together would be the best way to go about this. That would allow for some really cool functionality like easy parallax scrolling and a way to have timetables like this one:

screen shot 2014-06-09 at 23 38 57

Where you would be able to scroll on the x axis and the y axis, but the names of the stages (in the black rows) would only move on the y axis, but not on the x. So they stay in the middle. And the times at the top would only move on the x axis, but stay at the top when you scroll up and down.

evandsandler commented 10 years ago

@CoenWarmer, I see what you are saying, that would be a great UI design! After digging through the ionic code, i found that if you comment out or remove the "(e.defaultPrevented)" from the return statement in the ignoreScrollStart function then it allows scrolling to bubble up to parent scroll Views which would allow nested scroll views to work as desired. I haven't dug into the code to find out what potential side effects this might have though.

To achieve the UI you are talking about then, perhaps you could do this and then have each black line be something like a class "list-divider" then each row of red squares be a <ion-scroll direction="x>. To get the times row to stay at the top perhaps you could use something like bootstrap's affix(https://github.com/passy/angular-bootstrap-affix).

evandsandler commented 10 years ago

@CoenWarmer, perhaps a better and less 'hacky' way to do it on second thought is to make a method that checks if there is another scroll higher in the dom and prevents calling preventDefault at the end of self.touchStart in ionic.views.Scroll if it find there is a parent higher in the dom

CoenWarmer commented 10 years ago

Seems like your comment got cut off @evandsandler... But I'm definitely intrigued by this approach.

evandsandler commented 10 years ago

@CoenWarmer, you can just ignore my last comment until I figure out how to actually do it. The comment before though should help you get what you need hopefully. Let me know if it works for you or if my explanation of the changes to the ionic bundle were bad

ajoslin commented 10 years ago

@CoenWarmer it seems a better way to do what you want to do would be to have just one ion-scroll container, have the black bars be width: 100%, and then use a directive on the black bar's header element to keep it centered horizontally.

You can listen to the scroll event on the scroller and then broadcast that down as angular event, then have the headers catch it and align themselves correctly using JS.

CoenWarmer commented 10 years ago

Hey @ajoslin, I've made some progress in building the code that allows elements to fixed on one axis.

Code (in controller):

  $timeout(function(){
    angular.element(document.getElementsByClassName('timetable_scroll')[0]).on('scroll', function(e) {
      $scope.scrollOffset = [Math.abs(e.detail.scrollTop)+26,e.detail.scrollLeft];
      $scope.$$phase || $scope.$apply();
  },300);

Video on how it looks in action: https://www.youtube.com/watch?v=ZpTKjo21ZGI -> The time slots and the venue slots remain fixed when scrolling.

Only question left that would make this perfect. Right now I'm using a $timeout to execute this code in my controller. For some older devices, this timeout isn't long enough (maybe the DOM is still rendering? Unsure) so it gets called too early, and as a result $scope.scrollOffset never gets set.

I could up the timeout, but that would lead people with faster devices to wait unnecessarily. Is there a way to make this code execute only when the template is done rendering?

hvaoc commented 10 years ago

Hi @CoenWarmer, you can do the same using a directive instead of timeout.

app.directive('name', function() {
    return {
        link: function($scope, element, attrs) {
            // Trigger when number of children changes,
            // including by directives like ng-repeat
            var watch = $scope.$watch(function() {
                return element.children().length;
            }, function() {
                // Wait for templates to render
                $scope.$evalAsync(function() {
                    // Finally, directives are evaluated
                    // and templates are renderer here
                    var children = element.children();
                    console.log(children);
                });
            });
        },
    };
});

Taken from http://stackoverflow.com/questions/12304291/angularjs-how-to-run-additional-code-after-angularjs-has-rendered-a-template

hvaoc commented 10 years ago

@CoenWarmer Can you put your solution in CodePen.io too, I am trying to do something similar where I have two scrolls (Parent for horizontal scrolling of contents, some of the content in the horizontal scroll would have vertical list as scroll)

Everything works good, but the event touch & move (horizontally) on the child scroll (used for list) is not sent to the parent horizontal scroll.

paulseed commented 10 years ago

I've come up with pretty simple solution to this that seems passable. Add a ionicScrollDelegate handle to the mainScroll

<ion-content delegate-handle="mainScroll" has-bouncing="true" scroll="true">

Add drag events on the child/children ion-scroll(s)

<ion-scroll direction="x" scrollbar-y="false" scrollbar-x="false" on-drag-down="onDrag($event)" on-drag-up="onDrag($event)">

In the controller add the onDrag function and the ionicScrollDelegate to the scope

    $scope.handle = $ionicScrollDelegate.$getByHandle('mainScroll');

    $scope.onDrag = function(e){
        var distance = -1 * e.gesture.deltaY;
        $scope.handle.scrollBy(0,distance,true);
    };

I hope this helps.

sarim commented 10 years ago

@paulseed in html the delegate name is mainScroll, but in controller you called mainContent. Your code works after i set both to mainScroll. But this is not a viable solution, as the natural scroll speed and scrolled by controller speed aren't same. :(

I'll keep digging. IMO the clean solution would be emitting the touch/drag events to parent ion-content when user dragged in y axis. I need to find a function/callback something where i can choose not to call e.preventDefault() when dragging in y axis.

paulseed commented 10 years ago

Thank for the note on the typo! I fixed the the example. Anyway, after testing I realized that my above solution wasn't going to work, for the exact reasons you specified. So we came up with another solution. We created a directive that hijacks the touchStart event and doesn't call e.preventDefault().

app.directive('horizontalScroller',[function() {
  return {
    restrict:'E',
    scope:{
      content: '= data'
    },
     templateUrl: 'views/templates/horizontalScroller.html',
     link: function (scope, element) {

  //get a reference to the child ion-scroll
  var self = scope.scrollView;

  // I had to copy a couple functions from ion-scroll locally in order 
  // to make the new touch start function work
  function getPointerCoordinates(event) {
    // This method can get coordinates for both a mouse click
    // or a touch depending on the given event
    var c = { x:0, y:0 };
    if(event) {
      var touches = event.touches && event.touches.length ? event.touches : [event];
      var e = (event.changedTouches && event.changedTouches[0]) || touches[0];
      if(e) {
        c.x = e.clientX || e.pageX || 0;
        c.y = e.clientY || e.pageY || 0;
      }
    }
    return c;
  }

 //This was copied too
  function getEventTouches(e) {
    return e.touches && e.touches.length ? e.touches : [{
      pageX: e.pageX,
      pageY: e.pageY
    }];
  }

  // new touchstart function, only real difference is the 
  //call to e.preventDefault() is omitted
  var my_touchStart = function(e) {
    self.startCoordinates = getPointerCoordinates(e);
    if ( ionic.tap.ignoreScrollStart(e) ) {
      return;
    }

    self.__isDown = true;

    if( ionic.tap.containsOrIsTextInput(e.target) || e.target.tagName === 'SELECT' ) {
      // do not start if the target is a text input
      // if there is a touchmove on this input, then we can start the scroll
      self.__hasStarted = false;
      return;
    }

    self.__isSelectable = true;
    self.__enableScrollY = true;
    self.__hasStarted = true;
    self.doTouchStart(getEventTouches(e), e.timeStamp);

    //NOT GOING TO DO THIS!!!!
    //e.preventDefault();

    return false;
  };

   //replace ionic's touchstart with one of our own
  self.__container.removeEventListener("touchstart",scope.scrollView.touchStart);
  self.__container.addEventListener("touchstart",my_touchStart,false);
    }
  }
}]);

Template:

<ion-scroll direction="x" scrollbar-y="false" scrollbar-x="false">
    <div class="horizontalScroll" ng-bind-html="content"></div>
</ion-scroll>

The scrolling is now as you would expect. I have you say a big thanks to @jbasinger for all his help on this. DO WORK!!!! He's a ninja!

sarim commented 10 years ago

Interesting, I'll definitely try your solution, meanwhile i've come up with another solution too. Instead of monkey-patching the event listeners (which requires copying other related ionic codes to make it functional), i patched the event's preventDefault function.

        var sv = $ionicScrollDelegate.$getByHandle('slider').getScrollView();
        var container = sv.__container;

        var originaltouchStart = sv.touchStart;
        var originaltouchMove = sv.touchMove;

        container.removeEventListener('touchstart', sv.touchStart);
        document.removeEventListener('touchmove', sv.touchMove);

        sv.touchStart = function(e) {
            e.preventDefault = function(){}
            originaltouchStart.apply(sv, [e]);
        }

        sv.touchMove = function(e) {
            e.preventDefault = function(){}
            originaltouchMove.apply(sv, [e]);
        }

        container.addEventListener("touchstart", sv.touchStart, false);
        document.addEventListener("touchmove", sv.touchMove, false);

Currently i tested this from controller and it worked like a charm :3 . I'll need to move it to a directive now.

sarim commented 10 years ago

Update: While Above solutions works on iPhone, whole thing goes haywire on android o.O

paulseed commented 10 years ago

Our solution works on android. I have a feeling yours should too. I have only tested on 4.0+

olivierlesnicki commented 10 years ago

+1

osthafen commented 9 years ago

-> http://forum.ionicframework.com/t/when-using-horizontal-scroll-ion-scroll-page-cant-be-scrolled-down-in-y-direction/3833/16 -> http://forum.ionicframework.com/t/horizontal-scroll-problem/3383/12 I would love to have this solved within the next ionic release, too. ++ 1

bra1n commented 9 years ago

So, I just ran into the same problem, demonstrated here: http://codepen.io/anon/pen/BoGkA There is an ion-content container that holds the whole page and inside it I added an ion-scroll container that has some horizontally scrollable content: the big text in the demo. Try to scroll it horizontally, then while doing that also try to scroll the whole page vertically. Unfortunately, this does not work right now. The fix from @sarim shows, that it's perfectly possible if you could manually override the preventDefault for this specific scroll container. To enable it, just comment out line 4 (return false in the JS panel) What I'm suggesting now is, to add an attribute like scroll-parent to ion-scroll that toggles the aforementioned preventDefault on or off, both for touch and mouse events. I can submit a pull request for this, but I'm not sure if that would be the best approach, since I don't know where / how I should update documentation, unit tests and anything else besides the JS code. ;) In any case, this seems like an easy enough fix that would solve a (imo pretty big) usability problem of stacking scrollable containers inside each other. What do you think? I also tested it on Android (in the browser, not as a native app) and it worked perfectly fine for me.

ionitron-bot[bot] commented 6 years ago

Thanks for the issue! This issue is being locked to prevent comments that are not relevant to the original issue. If this is still an issue with the latest version of Ionic, please create a new issue and ensure the template is fully filled out.