Canvas 太慢了,Surfaceview 不显示内容

Canvas is to slow, Surfaceview doesn't show content

我正在创建一个 android 游戏。中间有一个需要一直旋转的圆圈。我首先创建了一个从 View 扩展的 Class,这样我就可以使用 onDraw 方法在 canvas 上绘制。它可以工作,但是当您玩了几分钟(有时甚至几秒钟后)时,游戏开始非常滞后。我在互联网上搜索解决方案。所以我发现我也可以使用 SurfaceView,其中绘图发生在单独的线程上。这样,当 ui 线程仍可用于所有用户交互时,我可以在另一个线程中完成所有绘图(我的游戏还包含很多动画) 但是我遇到了几个问题:

问题 1

当我开始在 SurfaceView 上绘图时,我的完整 SurfaceView 是黑色的。我搜索了这个问题并添加了这行代码:

    getHolder().setFormat(PixelFormat.TRANSLUCENT);

这有助于获得正确的背景颜色。但我的问题是:这是正确的做法吗?为什么我得到这个黑色叠加层(找不到很好的解释)

问题2

当我在表面视图上绘图时,我看不到任何绘图。为此,我在网上搜索并找到了这个:

    setZOrderOnTop(true);

这适用于展示我的图纸,但不是正确答案。我有一个 View "Game Over",当用户做错事时,它会显示在 surfaceview 的顶部。 那么我该如何解决这个问题,既可以看到我的绘图,又可以看到我的 GameOverView?

对于带有 onDraw(...) 的视图,我在 onDraw(...) 方法中计算了所有变量和绘图。因为我需要根据我的逻辑检查 canvas 的宽度和高度。

对于 Surfaceview,我使用带有此 Runnabable 运行 函数的线程:

@Override
public void run() {
    setZOrderOnTop(true);
    getHolder().setFormat(PixelFormat.TRANSLUCENT);
    while (isRunning) {
        if (!surfaceHolder.getSurface().isValid())
            continue;
        try {
            Thread.sleep(16);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Canvas canvas = surfaceHolder.lockCanvas();
        draw(canvas);
        surfaceHolder.unlockCanvasAndPost(canvas);
    }
}

编辑 1

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:ads="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="be.vanlooverenkoen.advancedwheel.ui.GameActivity">

    <be.vanlooverenkoen.advancedwheel.ui.CustomTextView
        android:id="@+id/pause_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentStart="true"
        android:clickable="true"
        android:padding="20dp"
        android:text="@string/pause"
        android:textSize="30sp" />

    <be.vanlooverenkoen.advancedwheel.ui.CustomTextView
        android:id="@+id/current_points_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:padding="20dp"
        android:text="@string/default_score"
        android:textSize="120sp" />

    <be.vanlooverenkoen.advancedwheel.ui.CustomTextView
        android:id="@+id/title_tv"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentStart="true"
        android:layout_alignParentTop="true"
        android:gravity="center"
        android:keepScreenOn="true"
        android:padding="16dp"
        android:text="@string/name_title"
        android:textSize="@dimen/title_font"
        android:textStyle="bold" />

    <LinearLayout
        android:id="@+id/best_score_layout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:clickable="true"
        android:gravity="end"
        android:orientation="vertical"
        android:paddingBottom="20dp"
        android:paddingTop="20dp">


        <be.vanlooverenkoen.advancedwheel.ui.CustomTextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="end"
            android:paddingEnd="20dp"
            android:paddingStart="20dp"
            android:text="BEST"
            android:textSize="15sp" />

        <be.vanlooverenkoen.advancedwheel.ui.CustomTextView
            android:id="@+id/high_score_tv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:paddingEnd="20dp"
            android:paddingStart="20dp"
            android:text="10"
            android:textSize="30sp" />
    </LinearLayout>

    <be.vanlooverenkoen.advancedwheel.ui.CircleView
        android:id="@+id/circleview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_above="@+id/control_panel"
        android:layout_below="@+id/title_tv" />

    <LinearLayout
        android:id="@+id/control_panel"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:gravity="center"
        android:orientation="vertical">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:orientation="horizontal">

            <ImageView
                android:id="@+id/volume_imgv"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:clickable="true"
                android:padding="20dp"
                android:src="@drawable/volume_on" />

            <ImageView
                android:id="@+id/settings_imgv"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:clickable="true"
                android:padding="20dp"
                android:src="@drawable/settings" />

            <ImageView
                android:id="@+id/infoImgv"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:clickable="true"
                android:padding="20dp"
                android:src="@drawable/info" />
        </LinearLayout>

        <com.google.android.gms.ads.AdView
            android:id="@+id/ad_banner"
            android:layout_width="wrap_content"
            android:layout_height="50dp"
            android:visibility="gone"
            ads:adSize="BANNER"
            ads:adUnitId="ca-app-pub-2228582628850456/4663114920" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:gravity="center"
        android:orientation="vertical">

        <be.vanlooverenkoen.advancedwheel.ui.CustomTextView
            android:id="@+id/replay_btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:padding="20dp"
            android:text="Replay"
            android:textSize="@dimen/title_font"
            android:visibility="gone" />

        <com.google.android.gms.ads.AdView
            android:id="@+id/gameover_ad_banner"
            android:layout_width="wrap_content"
            android:layout_height="50dp"
            android:visibility="gone"
            ads:adSize="BANNER"
            ads:adUnitId="ca-app-pub-2228582628850456/4663114920" />
    </LinearLayout>

    <be.vanlooverenkoen.advancedwheel.ui.CustomTextView
        android:id="@+id/game_over_tv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_centerInParent="true"
        android:gravity="center"
        android:keepScreenOn="true"
        android:padding="16dp"
        android:text="@string/game_over"
        android:textSize="@dimen/title_font"
        android:textStyle="bold" />

    <be.vanlooverenkoen.advancedwheel.ui.SettingsView
        android:id="@+id/settings_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clickable="true"
        android:padding="15dp" />

    <be.vanlooverenkoen.advancedwheel.ui.InfoView
        android:id="@+id/info_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clickable="true"
        android:padding="15dp" />

</RelativeLayout>

编辑 2 我的绘制方法:

private void draw(Canvas canvas) {
    int extra = (canvas.getWidth() - canvas.getHeight()) / 2;
    if (!isRunning)
        return;
    //The piechart of colors
    int offset = 0;
    if (canvas.getWidth() < canvas.getHeight()) {
        offset = canvas.getHeight() - canvas.getWidth();
        offset = offset / 2;
        rect.set(marginOuterCircle, marginOuterCircle + offset, canvas.getWidth() - marginOuterCircle, canvas.getWidth() + offset - marginOuterCircle);
    } else {
        rect.set(marginOuterCircle + extra, marginOuterCircle, canvas.getHeight() - marginOuterCircle + extra, canvas.getHeight() - marginOuterCircle);
    }
    //get value for 100%
    int sum = amountOfColors;
    //initalize painter
    paintPieChart.setStyle(Paint.Style.STROKE);
    paintPieChart.setStrokeWidth(1f);
    paintPieChart.setStyle(Paint.Style.FILL_AND_STROKE);
    double start = startAngle;
    for (int i = 0; i < amountOfColors; i++) {
        //draw slice
        paintPieChart.setColor(colors[i]);
        float angle;
        angle = ((360.0f / sum) * 1);
        if ((start <= 270 && start + angle >= 270) || (start <= -90 && start + angle >= -90)) {
            if (previousColor != paintPieChart.getColor()) {
                if (previousColor == paintStick.getColor()
                        && isPlaying)
                    wrongHit();
                if (inDemo && circleListener != null) {
                    circleListener.onWrongHit(score);
                }
                previousColor = currentColor;
            }
            if (inDemo && previousColor == paintStick.getColor() && circleListener != null)
                circleListener.onCorrectHit(0);
            currentColor = paintPieChart.getColor();
        }
        canvas.drawArc(rect, (float) start, angle, true, paintPieChart);
        start += angle;
        if (start > 360)
            start -= 360;
        if (start < -360)
            start += 360;
    }

    //Innercircle
    if (canvas.getWidth() < canvas.getHeight()) {
        rect.set(marginInnerCircle, marginInnerCircle + offset, canvas.getWidth() - marginInnerCircle, canvas.getWidth() + offset - marginInnerCircle);
    } else {
        //// TODO: 18/03/2017 FIX for height
        rect.set(marginInnerCircle + extra, marginInnerCircle, canvas.getHeight() - marginInnerCircle + extra, canvas.getHeight() - marginInnerCircle);
    }
    canvas.drawArc(rect, 0, 360, true, paintBackground);
    if (canvas.getWidth() < canvas.getHeight()) {
        rect.set(marginOverlayCircle, marginOverlayCircle + offset, canvas.getWidth() - marginOverlayCircle, canvas.getWidth() + offset - marginOverlayCircle);
    } else {
        //// TODO: 18/03/2017 FIX for height
        rect.set(marginOverlayCircle + extra, marginOverlayCircle, canvas.getHeight() - marginOverlayCircle + extra, canvas.getHeight() - marginOverlayCircle);
    }
    canvas.drawArc(rect, 0, 360, true, paintOverlay);
    //Stick
    rect.set((canvas.getWidth() / 2) - stickThickness
            , rect.top
            , (canvas.getWidth() / 2) + stickThickness
            , rect.bottom - ((rect.bottom - rect.top) / 2));
    canvas.drawRoundRect(rect, 2, 2, paintStick);

    if (revese)
        startAngle -= speed;
    else
        startAngle += speed;
    if (startAngle > 360)
        startAngle -= 360;
    if (startAngle < -360)
        startAngle += 360;
    index++;
    if (index >= colors.length)
        index = 0;
}

编辑 3

问题一:

答案在SurfaceView的draw()方法里面,你可以查看它的来源:

        @Override
        public void draw(Canvas canvas) {
            if (mWindowType != WindowManager.LayoutParams.TYPE_APPLICATION_PANEL) {
                // draw() is not called when SKIP_DRAW is set
                if ((mPrivateFlags & PFLAG_SKIP_DRAW) == 0) {
                    // punch a whole in the view-hierarchy below us
                    canvas.drawColor(0, PorterDuff.Mode.CLEAR);
                }
            }
            super.draw(canvas);
        }

所以它在做什么,是在视图层次结构中绘制黑色 - #000000 到 "punch a hole" 以不与任何其他视图的绘图重叠。默认像素格式是不透明的,所以这种颜色实际上变成了 #FF000000(不透明的黑色),因为系统不读取颜色的 alpha 通道。但是对于 PixelFormat.TRANSLUCENT,它使用适当的完全透明的黑色 - #00000000。 所以是的,这个解决方案是完全合法的。

您可以在此处阅读有关 SurfaceView 工作原理的详细信息 - https://source.android.com/devices/graphics/arch-sv-glsv

问题 2(编辑):

我建议在 SurfaceView 中手动绘制这个 Game Over 叠加层。

另一种可能的解决方案是设置 setZOrderOnTop(false); 并在帧之间使用纯色(非透明)清除背景。

您也可以尝试调用 setZOrderMediaOverlay(true)

确定有效的方法 - 将您的 SurfaceView 替换为 TextureView,因为它能够与屏幕上的其他视图正确组合 - https://source.android.com/devices/graphics/arch-tv

观察:

  • 您正在尝试互相渲染视图 above/under
  • 您无法在表面视图中看到任何呈现的内容
  • 在您的布局中,有多个视图覆盖了其他视图,有些甚至不可见

1) 在彼此之上渲染对象

您正在渲染彼此之上的视图,但许多视图在其他视图之上可见。这意味着视图 X 渲染在视图 Y 之上,视图 Y 被绘制到屏幕上但不可见。但是视图的可见性没有改变,因此您将资源浪费在不可见但应该可见的视图上。

2) 你在表面视图中看不到任何渲染的东西

(以下内容可能是错误的,因为必须用不同的视图替换很多视图才能使其在我的屏幕上正确呈现)

ID 为 "default_score" 的视图覆盖了 SurfaceView 的大部分。但是渲染慢反应慢的原因大概是这样的:

您将许多布局设置为 "match_parent",即使它们本应稍后显示。设置不需要的视图的可见性:

android:visibility="gone"
view.setVisibility(GONE);

其中之一,取决于您所在的位置。您只需要将(比如布局)设置为 GONE。这也会隐藏其子项,因此您无需更改所有子项的可见性。

将它们设置为 INVISIBLE 意味着它们仍然存在并且可以按下,因此它确实会占用更多内存。 GONE 意味着它不占用 space,不监听 touchevents 并且不渲染。这样可以节省内存并加快您的查看速度。

我在您的布局中看到两个 LinearLayouts 的子级设置为 GONE。改为将布局设置为 GONE,并删除对子项的可见性调用

3) 覆盖其他视图的视图

有几个观点覆盖了其他观点。这些视图未设置为 GONE(或不可见),可能会导致您遇到的问题。详情见文末摘要


解决方案建议

因为这是一个游戏,所以布局中有几样东西是你不需要一直使用的。以下是我的一些建议:

  • "game over" 屏幕上的全屏对话框,以及您不需要的其他内容始终可见。或者,将您的布局拆分为多个视图。每个视图将包含游戏的一部分。如果您不需要视图 1 可见,请将其设置为 GONE 并显示视图 2 或 3。(在这种情况下,3 只是一个随机数。您可以根据需要将布局分成多个部分)。任何同时不可见的东西都应该设置为 GONE(意味着父布局应该设置为 INVISIBLE。它的子布局受它影响)
  • 确保不可见视图(可见性设置为 GONE)是您不需要的视图。并确保在给定时间不需要显示的所有视图都将其可见性设置为 GONE

您的截图:

我在这里看到的是两个重叠:"advanced wheel" 和数字“0”。

"game over" 和你的表面视图。将启动时不可见的设置为"GONE"。

尽可能切换到线性布局。这意味着如果两个条(顶部和底部)和 surfaceview 是线性放置的,则不需要将其作为 RelativeLayout。如果条形图的内容不是线性的(意味着您不能使用 LinearLayout 获得该结果),请使用 Relative(or COnstraint)Layout

将实例结束内容(游戏结束、总分、重播按钮、主菜单按钮等)移动到全屏对话框。如果你想使用它,你可以在全屏对话框中添加一个 SurfaceView,或者你有一些你想要绘制的内容。我建议您在这部分使用 DialogFragment。您还可以将得分、SurfaceView class(如果它包含游戏逻辑)等内容传递给此对话框。然后,在按钮上,你可以做一些事情,比如调用 SurfaceView 来重播和关闭对话框,或者获取 activity 并创建一个意图去菜单


对一些评论的回答

I just tried to remove the AdView from admob and the app goes a lot smoother? How is this possible?

可能是因为您加载了它,这意味着它想要(并且确实)呈现,但未显示。您使用了不应该使用的资源(在甚至不可见的 admob 横幅上)。我没有看到你的那部分代码,所以这可能是错误的。

But the biggest question and most important one is why is it so slow?

当您是 运行 一个 SurfaceView,并且在其上方和旁边绘制了大量视图时,您必须考虑几件事情。我的建议是将主视图(即根视图)更改为 LinearLayout。因此,您可以排列不太复杂的视图(例如条形图布局等),但例如条形图的内容是 RelativeLayouts。 LinearLayout 使用较少的内存,因为它(很好)是线性的。它不需要存储"this is rendered here and this is rendered here above this and under that",它存储顺序和width/height