codenameone / CodenameOne

Cross-platform framework for building truly native mobile apps with Java or Kotlin. Write Once Run Anywhere support for iOS, Android, Desktop & Web.
https://www.codenameone.com/
Other
1.69k stars 403 forks source link

Flickering BoxLayout Y #2320

Open kutoman opened 6 years ago

kutoman commented 6 years ago

this happens on Android, iOs and the simulator. You need to disable tensile drag in order to encounter the effect.

jitteringissue

As you can see here, the flickering is triggered when the end of the Container is reached. But not every time.

codenameone commented 6 years ago

I think this will need a test case too, you have a lot of content there and it isn't clear what type of content.

Why are you disabling the tensile drag?

kutoman commented 6 years ago

actually the reason why I disabled tensile dragging for our app in production is that tensile drag also causes a similar issue. Occasionally during the drag it happens that the content of the layout completely slides out of the view.

This is a sample code to reproduce. The issue seems to be caused due to the header animation:

        List<Component> listItems = new ArrayList<>();
        for(int i = 0; i < 20; i++) listItems.add(new Label("ITEM " + i));
        Label l = new Label("THIS HEADER IS LAME");

        Container first = BoxLayout.encloseY(listItems.toArray(new Component[listItems.size()]));
        first.setScrollableY(true);
        first.setTensileDragEnabled(false);
        Container second = BoxLayout.encloseX(new Label("Page"));

        Form form = new Form(new BorderLayout());
        form.add(BorderLayout.NORTH, l);
        form.add(BorderLayout.CENTER, first);

        OnScrollHideAnimator osha = new OnScrollHideAnimator(l);
        osha.subscribe(first);

        form.getToolbar().setHidden(true);
        form.show();

....

import com.codename1.charts.util.ColorUtil;
import com.codename1.ui.CN;
import com.codename1.ui.Component;
import com.codename1.ui.Container;
import com.codename1.ui.Graphics;
import com.codename1.ui.Image;
import com.codename1.ui.animations.ComponentAnimation;
import com.codename1.ui.layouts.LayeredLayout;
import com.codename1.ui.plaf.Style;
import java.util.ArrayList;
import java.util.List;

/**
 *
 * @author SoftwareUNIT
 */
public class OnScrollHideAnimator {

    private static final int ANIMATION_DURATION = 400;
    private static final String HIDDEN_STATE_UIID = "Empty";
    private static final String SHOWN_STATE_UIID_PREFIX = "Unhidden_";

    private final Logging logger = Logging.get(this);

    private final Component hideableComponent;
    private final Container hideablesParent;

    private Component activeDummy;
    private boolean isAnimating;

    private final List<Container> scrollableSubscribers;

    private Component focusedSubscriber;

    public OnScrollHideAnimator(Component hideableComponent) {
        this(hideableComponent, 0);
    }

    public OnScrollHideAnimator(Component hideableComponent, int heightInPixelsWhenHidden) {
        this.hideableComponent = hideableComponent;
        this.hideablesParent = hideableComponent.getParent();
        this.scrollableSubscribers = new ArrayList<>();

        if(hideablesParent == null) throw new IllegalStateException("the hideable component seems not to be part of the UI!");

        Style hiddenStyle = hideableComponent.getUIManager().getComponentStyle(HIDDEN_STATE_UIID);
        hiddenStyle.setPaddingUnit(Style.UNIT_TYPE_PIXELS);
        hiddenStyle.setPadding(Component.TOP, heightInPixelsWhenHidden);
        hiddenStyle.setBgTransparency(0);
        hideableComponent.getUIManager().setComponentStyle(HIDDEN_STATE_UIID, hiddenStyle);
    }

    /**
     * If already subscribed before the given subscriber gets focus
     * @param scrollableContent 
     */
    public void subscribe(Container scrollableContent) {

        boolean isMySubscriber = scrollableSubscribers.contains(scrollableContent); //(Boolean) scrollableContent.getPropertyValue(IS_MY_SUBSCRIBER_PROP);

        focusedSubscriber = scrollableContent;
        onGainFocus();
        if(isMySubscriber) return;
        logger.p("subscribed " + scrollableContent);

        scrollableContent.addScrollListener((int scrollX, int scrollY, int oldscrollX, int oldscrollY) -> {

            //if(scrollableContent != focusedSubscriber) return;

            int maxY = scrollableContent.getScrollDimension().getHeight() - scrollableContent.getHeight();
            if(scrollY <= 0 || scrollY >= maxY) {}
            else if(scrollY > oldscrollY) animateHide(false);
            else if(scrollY < oldscrollY) animateHide(true);
        });

        //scrollableContent.setPropertyValue(IS_MY_SUBSCRIBER_PROP, true);
        scrollableSubscribers.add(scrollableContent);
    }

    private void onGainFocus() {
        removeEffect();
    }

    public void removeEffect() {
        if(activeDummy != null && activeDummy.getParent() != null) {
            animateHide(true);
        }
    }

    public void animateHide(boolean reversed) {

        if(isAnimating) return;

        if(reversed) {
            if(activeDummy == null || activeDummy.getParent() == null) return;
            isAnimating = true;

            CN.callSerially(() -> {
                hideablesParent.revalidate();
                ComponentAnimation ca = activeDummy.createStyleAnimation(SHOWN_STATE_UIID_PREFIX + hashCode(), ANIMATION_DURATION);

                hideablesParent.getAnimationManager().addAnimation(ca, () -> {

                    if(activeDummy != null && activeDummy.getParent() != null) {
                        if(hideableComponent.getParent() == null) {
                            hideablesParent.replace(activeDummy, hideableComponent, null);
                            hideablesParent.revalidate();

                            activeDummy = null;
                            isAnimating = false;
                            //logger.p("shown");
                        }
                    }

                });
            });
            //logger.p("showing..");

        } else {

            if(hideableComponent.getParent() == null) return;
            isAnimating = true;

            CN.callSerially( () -> {
                Component dummy = createHideableComponentsDummy();

                hideablesParent.replaceAndWait(hideableComponent, dummy, null);
                hideablesParent.revalidate();

                ComponentAnimation ca = dummy.createStyleAnimation(HIDDEN_STATE_UIID, ANIMATION_DURATION);
                hideablesParent.getAnimationManager().addAnimation(ca, () -> {
                    hideablesParent.revalidate();
                    isAnimating = false; 
                    //logger.p("hidden");
                });
                //logger.p("hiding..");

                activeDummy = dummy;
            });

        }
    }

    private Component createHideableComponentsDummy() {

        if(hideableComponent.isHidden()) throw new IllegalStateException("component mustn't be hidden for the dummy!");

        Container dummy = LayeredLayout.encloseIn();
        dummy.setUIID("Transparent");

        Image dummyImg = Image.createImage(hideableComponent.getWidth(), hideableComponent.getHeight(), ColorUtil.WHITE);
        Graphics g = dummyImg.getGraphics();

        if(hideableComponent instanceof Container) ((Container) hideableComponent).paintComponentBackground(g);
        hideableComponent.paint(g);

        Style dummyStyle = dummy.getAllStyles();
        Style initialStyle = new Style(dummyStyle);

        initialStyle.setBackgroundType(Style.BACKGROUND_IMAGE_ALIGNED_BOTTOM);
        initialStyle.setBgImage(dummyImg);
        initialStyle.setPaddingUnit(Style.UNIT_TYPE_PIXELS);
        initialStyle.setPadding(dummyImg.getHeight(), 0, dummyImg.getWidth(), 0);
        initialStyle.setMargin(0, 0, 0, 0);
        dummyStyle.merge(initialStyle);

        hideableComponent.getUIManager().setComponentStyle(SHOWN_STATE_UIID_PREFIX + hashCode(), initialStyle);

        return dummy;
    }
}

preview:

scroll-flickering-issue

kutoman commented 6 years ago

this code helps to avoid this effect (when tensile dragging is disabled):

@Override
protected void onScrollY(int scrollY) {
    int fullHeight = getScrollDimension().getHeight();
    int height = getHeight();

    int maxScrollY = fullHeight - height;

    if(scrollY == 0) {}
    else if(scrollY > maxScrollY && forceBlockDownwards) super.setScrollY(Math.max(0, maxScrollY));
    else if(scrollY < 0 && forceBlockUpwards) super.setScrollY(0);

}
codenameone commented 6 years ago

Why aren't you using title animation? It looks like you are adding regular animations which are triggering repaints while you are scrolling all the way down. Extra repaints == flickering.

kutoman commented 6 years ago

onTitleScrollAnimation does not produce the same UX since it is based on the current state of the scrolling (e.g. the animation can be paused when scrolling is paused). What I need, are just four states: Idle(visible), reshowing, completely hidden, hiding. Like in WhatsApp for example

codenameone commented 6 years ago

I think you can probably create an animator that replicates that behavior and don't have to use a style animator. What you do need to be aware of is repaints. You are invoking revalidate a lot which you shouldn't. That will trigger flickering because you are literally asking the entire component tree to lay itself again while the user is scrolling.

kutoman commented 6 years ago

but revalidate is only called once on each state transition (showing->hide animation->hidden, hidden->show animation->showing) and the problem does not occur when I disable tensile dragging (plus the mentioned code to clamp scrollY). Do you have a specific suggestion how I could manage this by the animator? I couldn't figure it out.

codenameone commented 6 years ago

You have a lot of revalidates all over and most of them seem redundant. You have a replace and wait that might happen during scrolling. On its own it might be OK but with revalidates and other animations a lot of these things can collide. You need to minimize the concurrent moving parts.

kutoman commented 6 years ago

when I first implemented this I had less revalidates. But I noticed that they are necessary at those places because the changes weren't visible on iOS otherwise. I totally would prefer another solution with less need for revalidates/replacements but I couldn't figure it out. I couln't find another way to make such animations. By the way, wouldn't it be better, when CN1's api would provide this common animation type out of the box? Apps like WhatsApp, Youtube, Google Play etc do use this animation and as said CN1's title animation is not the same animation

codenameone commented 6 years ago

This is actually builtin. It's the toolbar hiding feature that's built right into toolbar. See Toolbar.setScrollOffUponContentPane(true);

kutoman commented 6 years ago

@codenameone thanks I was exaclty looking for this. But unfortunately It doesn't work for me (when scrolling). Did I miss something?

    Form f = new Form(new BorderLayout());

    Button btn = new Button("test");
    btn.addActionListener((evt) -> f.getToolbar().hideToolbar());

    f.getToolbar().add(BorderLayout.EAST, btn);
    f.getToolbar().setScrollOffUponContentPane(true);
    f.setScrollableY(true);

    List<Component> listItems = new ArrayList<>();
    for(int i = 0; i < 20; i++) listItems.add(new Label("ITEM " + i));
    Container scrollable = BoxLayout.encloseY(listItems.toArray(new Component[listItems.size()]));
    scrollable.setScrollableY(true);

    f.add(BorderLayout.CENTER, scrollable);
    f.revalidate();

    f.show();
kutoman commented 6 years ago

ok I see now. The scrollable content needs to be in the content pane of f directly in order to get this working.. Isn't it possible to link a nested scrollable content instead? Use Case: when the actual scrollable content is in a Tabs component.

codenameone commented 6 years ago

I wanted to do that back when it was introduced but the API became awkward I ended up abandoning that. I agree that would be nice. We've got a bit of a packed schedule right now so I'm not sure if we have time right now for another RFE. If you want to submit one we can close this issue and file an RFE but I'll probably assign it to 6.0.

The code is relatively simple and very localized to Toolbar.java if you are looking for a perfect example for a pull request this looks like a good option. No pressure though ;-)

kutoman commented 6 years ago

I could but first this should be fixed (which wouldn't be easy for me):

2394