如何实现基于手势的菜单

时间:2012-06-15 04:43:33

标签: menu

我应该实现一个基于手势的菜单,您可以通过平移或扔过它们来滚动浏览水平项目列表。这种菜单在智能手机游戏中很常见。示例案例将是Cut the Rope,您可以在其中选择框(Cardboard box,Fabric box)或Angry Birds,您可以在其中选择一组级别(Poached Eggs,Mighty Hoax)。

我在想的是,我必须做一些复杂的物理计算,并根据手势给出菜单项的速度和加速度。更好的解决方案?我正在使用libgdx btw。

1 个答案:

答案 0 :(得分:5)

我认为您不需要通过所有这些来实现简单的菜单!这都是关于为各种项目定义偏移量(我只是假设您想要切割绳索式菜单,在给定时刻只有一个条目(不包括过渡))然后在那些之间补间每当检测到轻弹时都会发生偏移!

您似乎已将所有手势系统连接起来,所以现在,我们只需要弄清楚如何显示菜单。 为简单起见,我们假设我们不希望菜单环绕。

我们首先想象一下这个菜单在我们脑海中的样子。它就像一部通过手机的幻灯片,可以在屏幕上看到。

                   phone ("stuff" is currently selected)
                  ========  
 |---------------|        |-------|
 | Start | About | Stuff  | Quit  |
 |---------------|        |-------|
                 |        |
                 |        |
                 |        |
                  ========

我们假设屏幕宽度为 w ,因此,所有菜单条目都是该宽度(想想再次剪切绳索!)。

现在,当要显示“开始”时,我们应该在屏幕上以第一个元素“开始”开始渲染flimstrip,而理论上,其余部分将位于屏幕右侧。这将被视为基本情况,使用 offset = 0

呈现菜单

是的,是的,这个偏移将是我们的小滑动滑动菜单的关键!现在,非常明显的是,当选择about时,我们只需要将“filmstrip”向左偏移一个“帧”,这里 offset = - 1 * frameWidth 。我的精彩ASCII艺术所示的示例案例选择了第三个菜单项,并且由于帧从0开始索引,我们只需减去frameWidth的两倍并获得所需的偏移量。我们只是从offset = -2 * frameWidth开始渲染菜单。

(显然你可以提前计算frameWidth,使用API​​获取屏幕宽度,然后只绘制菜单元素text / graphic居中)。

所以这很简单:

  • 用户向左扫描,我们需要更接近偏移0的菜单,我们将所选实体的索引减一,然后菜单跳转到正确的位置

  • 用户向右扫描,我们增加了索引(显然只要它没有超过菜单元素的数量 - 1)

但是补间顺畅呢? 谢天谢地,Libgdx已经为好的小补间设置了插值。我们只需要照顾好一些事情,这样我们就不会在腿上射击。我会在这里列出来的。

一个快速说明: Cut the Rope级别选择器的工作方式与我在这里所说的不同。它不仅对电影(预定义的手势)做出反应,而且更敏感。您可以通过播放偏移并跟踪手指在屏幕上的位置来实现类似的效果。 (如果用户将菜单条目向左/向右拖动太多,则自动转换到上一个/下一个)友好建议:只需设置一个简单的工作菜单,并将这样的详细信息留到最后,因为它们最终会结束花了很多时间 :P

好的,回到正轨!

我们现在拥有的是一种快速切换偏移的方法。我们只需要补间。还有一些其他成员发挥作用,但我认为他们是非常不言自明的。当我们在两个元素之间转换时,我们会记住“旧的”偏移,我们正在前进的那个,以及记住我们从转换中离开的时间,并且我们使用这四个变量来计算偏移量(在当前时刻使用libgdx插值,在这种情况下为exp10,从而产生平滑的动画。

让我们看看,我已经创建了一个快速的模拟。我尽可能地评论了代码,所以我希望下面的代码片段能说明一切! :d

import java.util.ArrayList;

import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.Interpolation;

public class MenuManager {

// The list of the entries being iterated over
private ArrayList<MenuEntry> entries = new ArrayList<>();

// The current selected thingy
private int index;

// The menu offset
private float offset = 0.0f;

// Offset of the old menu position, before it started tweening to the new one
private float oldOffset = 0.0f;

// What we're tweening towards
private float targetOffset = 0.0f;

// Hardcoded, I know, you can set this in a smarter fashion to suit your
// needs - it's basically as wide as the screen in my case
private float entryWidth = 400.0f;

// Whether we've finished tweening
private boolean finished = true;

// How much time a single transition should take
private float transitionTimeTotal = 0.33f;

// How much time we have left from the current transition
private float transitionTimeLeft = 0.0f;

// libgdx helper to nicely interpolate between the current and the next 
// positions
private Interpolation interpolation = Interpolation.exp10;

public void addEntry(MenuEntry entry) {
    entries.add(entry);
}

// Called to initiate transition to the next element in the menu
public void selectNext() {
    // Don't do anything if we're still animationg
    if(!finished) return;

    if(index < entries.size() - 1) {
        index++;
        // We need to head towards the next "frame" of the "filmstrip"
        targetOffset = oldOffset + entryWidth;
        finished = false;
        transitionTimeLeft = transitionTimeTotal;
    } else {
        // (does nothing now, menu doesn't wrap around)
        System.out.println("Cannot go to menu entry > entries.size()!");
    }
}

// see selectNext()
public void selectPrevious() {
    if(!finished) return;

    if(index > 0) {
        index --;
        targetOffset = oldOffset - entryWidth;
        finished = false;
        transitionTimeLeft = transitionTimeTotal;
    } else {
        System.out.println("Cannot go to menu entry <0!");
    }
}

// Called when the user selects someting (taps the menu, presses a button, whatever)
public void selectCurrent() {
    if(!finished) {
        System.out.println("Still moving, hold yer pants!");
    } else {
        entries.get(index).select();
    }
}

public void update(float delta) {

    if(transitionTimeLeft > 0.0f) {
        // if we're still transitioning

        transitionTimeLeft -= delta;
        offset = interpolation.apply(oldOffset, targetOffset, 1 - transitionTimeLeft / transitionTimeTotal);
    } else {
        // Transition is over but we haven't handled it yet
        if(!finished) {
            transitionTimeLeft = 0.0f;
            finished = true;
            oldOffset = targetOffset;
        }
    }
}

// Todo make font belong to menu
public void draw(SpriteBatch spriteBatch, BitmapFont font) {
    if(!finished) {
        // We're animating, just iterate through everything and draw it,
        // it's not like we're wasting *too* much CPU power
        for(int i = 0; i < entries.size(); i++) {
            entries.get(i).draw((int)(i * entryWidth - offset), 100, spriteBatch, font);
        }
    } else {
        // We're not animating, just draw the active thingy
        entries.get(index).draw(0, 100, spriteBatch, font);
    }
}

}

我相信一个简单的基于文本的菜单条目可以自己绘制就足够了! (请注意脏的硬编码文本包装宽度!

public class MenuEntry {


private String label;
// private RenderNode2D graphic;
private Action action;

public MenuEntry(String label, Action action) {
    this.label = label;
    this.action = action;
}

public void select() {
    this.action.execute();
}

public void draw(int x, int y, SpriteBatch spriteBatch, BitmapFont font) {
    font.drawMultiLine(spriteBatch, label, x, y, 400, HAlignment.CENTER);
}

}

哦,行动只是一个有执行方法的东西,而且代表一个动作。

public interface Action {
abstract void execute();

}

随意在评论中提出任何相关问题,我会尝试澄清所需要的内容。 希望这有帮助!