Android, Canvas: Wie lösche (lösche) ich einen Canvas (= Bitmaps), der in einer SurfaceView lebt?


92

Um ein einfaches Spiel zu erstellen, habe ich eine Vorlage verwendet, die eine Leinwand mit Bitmaps wie folgt zeichnet:

private void doDraw(Canvas canvas) {
    for (int i=0;i<8;i++)
        for (int j=0;j<9;j++)
            for (int k=0;k<7;k++)   {
    canvas.drawBitmap(mBits[allBits[i][j][k]], i*50 -k*7, j*50 -k*7, null); } }

(Die Zeichenfläche ist in "run ()" definiert / die SurfaceView befindet sich in einem GameThread.)

Meine erste Frage ist, wie ich die gesamte Leinwand für ein neues Layout löschen (oder neu zeichnen) kann .
Zweitens, wie kann ich nur einen Teil des Bildschirms aktualisieren?

// This is the routine that calls "doDraw":
public void run() {
    while (mRun) {
        Canvas c = null;
        try {
            c = mSurfaceHolder.lockCanvas(null);
            synchronized (mSurfaceHolder) {
                if (mMode == STATE_RUNNING) 
                    updateGame();
                doDraw(c);          }
        } finally {
            if (c != null) {
                mSurfaceHolder.unlockCanvasAndPost(c);  }   }   }       }

Antworten:


77

Wie lösche (oder zeichne) ich die GANZE Leinwand für ein neues Layout (= versuche das Spiel)?

Rufen Canvas.drawColor(Color.BLACK)Sie einfach an oder mit welcher Farbe Sie Ihre löschen möchten Canvas.

Und: Wie kann ich nur einen Teil des Bildschirms aktualisieren?

Es gibt keine solche Methode, die nur einen "Teil des Bildschirms" aktualisiert, da das Android-Betriebssystem beim Aktualisieren des Bildschirms jedes Pixel neu zeichnet. Wenn Sie jedoch keine alten Zeichnungen auf Ihrem CanvasBildschirm löschen , befinden sich die alten Zeichnungen immer noch auf der Oberfläche. Dies ist wahrscheinlich eine Möglichkeit, "nur einen Teil des Bildschirms zu aktualisieren".

Wenn Sie also "einen Teil des Bildschirms aktualisieren" möchten, vermeiden Sie einfach den Aufruf der Canvas.drawColor()Methode.


4
Nein, wenn Sie nicht jedes Pixel in der Oberfläche zeichnen, erhalten Sie sehr seltsame Ergebnisse (da durch das Vertauschen von Zeigern eine doppelte Pufferung erreicht wird, sehen Sie in den Teilen, in denen Sie nicht zeichnen, nicht, was gerade zuvor vorhanden war). Sie müssen jedes Pixel der Oberfläche bei jeder Iteration neu zeichnen.
Guillaume Brunerie

2
@Viktor Lannér: Wenn Sie anrufen mSurfaceHolder.unlockCanvasAndPost(c)und dann c = mSurfaceHolder.lockCanvas(null), dann enthält das neue cnicht das gleiche wie das vorherige c. Sie können nicht nur einen Teil einer SurfaceView aktualisieren, was das OP wohl gefragt hat.
Guillaume Brunerie

1
@ Guillaume Brunerie: Stimmt, aber nicht alle. Sie können einen Teil des Bildschirms nicht aktualisieren, wie ich geschrieben habe. Sie können jedoch alte Zeichnungen auf dem Bildschirm behalten, was dazu führt, dass nur "ein Teil des Bildschirms aktualisiert" wird. Probieren Sie es selbst in einer Beispielanwendung aus. Canvashält alte Zeichnungen.
Wroclai

2
SCHANDE ÜBER MICH !!! Ich habe mein Array nur EINMAL beim Spielstart initialisiert, NICHT beim aufeinanderfolgenden Neustart - also blieben ALLE ALTEN Teile "auf dem Bildschirm" - sie wurden nur neu gezeichnet !!! Ich bin sehr traurig über meine "Dummheit"! Danke euch beiden !!!
SamClem

1
Dies liegt wahrscheinlich daran, dass Live-Hintergründe nicht wie normale SurfaceView funktionieren. Wie auch immer, @samClem: Zeichne immer jedes Pixel der Leinwand an jedem Frame neu (wie im Dokument angegeben), sonst kommt es zu seltsamem Flackern.
Guillaume Brunerie

277

Das Zeichnen transparenter Farben im PorterDuff-Löschmodus macht den Trick für das, was ich wollte.

Canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)

2
Dies sollte funktionieren, scheint jedoch in 4.4 (zumindest auf N7.2) fehlerhaft zu sein. Verwenden Sie diese Bitmap#eraseColor(Color.TRANSPARENT)Option wie in der Antwort von HeMac unten.
nmr

3
In der Tat Color.TRANSPARENTist unnötig. PorterDuff.Mode.CLEARist völlig ausreichend für eine ARGB_8888Bitmap, was bedeutet, dass Alpha und Farbe auf [0, 0] gesetzt werden. Eine andere Möglichkeit ist die Verwendung Color.TRANSPARENTmit PorterDuff.Mode.SRC.
Jiasli

Dies funktioniert nicht für mich, wenn ich eine leere Oberfläche anstelle von Transparent
Pallav Bohara am

30

Fand dies in Google Groups und das hat bei mir funktioniert ..

Paint clearPaint = new Paint();
clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
canvas.drawRect(0, 0, width, height, clearPaint); 

Dadurch werden Zeichnungsrechtecke usw. entfernt, während die festgelegte Bitmap beibehalten wird.


4
Dies gibt mir einen schwarzen Bereich - nicht die Bitmap, die ich dahinter habe :(
Slott

Es wird gut gelöscht, aber die Bitmap wird auch entfernt, wie Sie gesagt haben.
Omar Rehman

In stackoverflow.com/questions/9691985/… wird erläutert, wie Sie ein Rechteck einer Bitmap auf ein Rechteck der Leinwand zeichnen. Ändern Sie ein Bild in einem Rechteck so funktioniert: Lear den vorherigen Inhalt, zeichnen Sie das neue Bild
Martin

18

Ich habe die Antwort von @mobistry ausprobiert:

canvas.drawColor(Color.TRANSPARENT, Mode.CLEAR);

Aber es hat bei mir nicht funktioniert.

Die Lösung war für mich:

canvas.drawColor(Color.TRANSPARENT, Mode.MULTIPLY);

Vielleicht hat jemand das gleiche Problem.


4
Mit Transparenz multiplizieren ist die Antwort. Andernfalls kann es auf einigen Geräten schwarz werden.
Andreas Rudolph

16

Verwenden Sie die Rücksetzmethode der Path-Klasse

Path.reset();

Dies ist wirklich die beste Antwort, wenn Sie einen Pfad verwenden. Die anderen können den Benutzer mit einem schwarzen Bildschirm verlassen. Vielen Dank.
j2emanue

12
mBitmap.eraseColor(Color.TRANSPARENT);

canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);

2
Wie ist das am richtigsten? Warum überhaupt eine Bitmap verwenden, wenn Sie nur Farbe zeichnen können?
RichieHH

Obwohl dies für das Originalposter möglicherweise nicht funktioniert (sie haben nicht unbedingt Zugriff auf die Bitmap), ist dies auf jeden Fall nützlich, um den Inhalt einer Bitmap zu löschen, wenn diese verfügbar ist. In diesen Fällen ist nur die erste Zeile erforderlich. Nützliche Technik, +1
Kopf in den Codes

Kannst du einen ganzen Code schreiben?
Dyno Cris

4

Bitte fügen Sie den folgenden Code in die Oberflächenansicht ein. Erweitern Sie den Klassenkonstruktor .............

Konstruktorcodierung

    SurfaceHolder holder = getHolder();
    holder.addCallback(this);

    SurfaceView sur = (SurfaceView)findViewById(R.id.surfaceview);
    sur.setZOrderOnTop(true);    // necessary
    holder = sur.getHolder();
    holder.setFormat(PixelFormat.TRANSPARENT);

XML-Codierung

    <com.welcome.panelview.PanelViewWelcomeScreen
        android:id="@+id/one"
        android:layout_width="600px"
        android:layout_height="312px"
        android:layout_gravity="center"
        android:layout_marginTop="10px"
        android:background="@drawable/welcome" />

versuchen Sie oben Code ...


Inhaber.setFormat (PixelFormat.TRANSPARENT); Für mich geht das.
Aaron Lee

3

Hier ist der Code eines minimalen Beispiels, das zeigt, dass Sie immer jedes Pixel der Leinwand bei jedem Frame neu zeichnen müssen.

Diese Aktivität zeichnet jede Sekunde eine neue Bitmap in der SurfaceView, ohne zuvor den Bildschirm zu löschen. Wenn Sie es testen, werden Sie feststellen, dass die Bitmap nicht immer in denselben Puffer geschrieben wird und der Bildschirm zwischen den beiden Puffern wechselt.

Ich habe es auf meinem Handy (Nexus S, Android 2.3.3) und auf dem Emulator (Android 2.2) getestet.

public class TestCanvas extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new TestView(this));
    }
}

class TestView extends SurfaceView implements SurfaceHolder.Callback {

    private TestThread mThread;
    private int mWidth;
    private int mHeight;
    private Bitmap mBitmap;
    private SurfaceHolder mSurfaceHolder;

    public TestView(Context context) {
        super(context);
        mThread = new TestThread();
        mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.icon);
        mSurfaceHolder = getHolder();
        mSurfaceHolder.addCallback(this);
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width,
            int height) {
        mWidth = width;
        mHeight = height;
        mThread.start();
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {/* Do nothing */}

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        if (mThread != null && mThread.isAlive())
            mThread.interrupt();
    }

    class TestThread extends Thread {
        @Override
        public void run() {
            while (!isInterrupted()) {
                Canvas c = null;
                try {
                    c = mSurfaceHolder.lockCanvas(null);
                    synchronized (mSurfaceHolder) {
                        c.drawBitmap(mBitmap, (int) (Math.random() * mWidth), (int) (Math.random() * mHeight), null);
                    }
                } finally {
                    if (c != null)
                        mSurfaceHolder.unlockCanvasAndPost(c);
                }

                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    interrupt();
                }
            }
        }   
    }
}

Nun, es scheint, dass Sie falsch liegen. Sehen Sie sich hier meinen Screenshot an: img12.imageshack.us/i/devicey.png/# Wenn Sie eine Verzögerung von einer Sekunde hinzugefügt haben, wird die doppelte Pufferung stärker wahrgenommen, aber (!) Es sind noch frühere Zeichnungen auf dem Bildschirm. Außerdem ist Ihr Code falsch: Es sollte SurfaceHolder.Callbacknicht nur so sein Callback.
Wroclai

1
Ich denke du verstehst nicht was ich meine. Was man erwarten könnte ist, dass der Unterschied zwischen Frame n und Frame n + 1 darin besteht, dass es noch eine Bitmap gibt. Aber das ist völlig falsch, es gibt noch eine Bitmap zwischen Frame n und Frame n + 2 , aber Frame n und Frame n + 1 haben nichts miteinander zu tun, selbst wenn ich gerade eine Bitmap hinzugefügt habe.
Guillaume Brunerie

Nein, ich verstehe dich voll und ganz. Aber wie Sie sehen, funktioniert Ihr Code nicht. Nach einer Weile ist der Bildschirm voll mit Symbolen. Daher müssen wir das löschen, Canvaswenn wir den gesamten Bildschirm vollständig neu zeichnen möchten. Das macht meine Aussage über "frühere Zeichnungen" wahr. Das Canvashält vorherige Zeichnungen.
Wroclai

4
Ja, wenn Sie möchten, Canvasbehält a frühere Zeichnungen bei. Dies ist jedoch völlig nutzlos. Das Problem besteht darin, dass Sie bei der Verwendung lockCanvas()nicht wissen, was diese „vorherigen Zeichnungen“ sind, und nichts davon annehmen können. Wenn zwei Aktivitäten mit einer SurfaceView derselben Größe vorhanden sind, teilen sie möglicherweise dieselbe Leinwand. Vielleicht bekommen Sie einen Teil des nicht initialisierten RAM mit zufälligen Bytes. Vielleicht ist immer das Google-Logo im Canvas geschrieben. Du kannst es nicht wissen. Jede Anwendung, die nicht jedes Pixel nachher lockCanvas(null)zeichnet, ist fehlerhaft.
Guillaume Brunerie

1
@ GuillaumeBrunerie 2,5 Jahre nach der Tatsache, dass ich auf Ihren Beitrag gestoßen bin. Die offizielle Android-Dokumentation unterstützt Ihre Ratschläge zum "Zeichnen jedes Pixels". Es gibt nur einen Fall, in dem ein Teil der Zeichenfläche bei einem nachfolgenden Aufruf von lockCanvas () garantiert noch vorhanden ist. Weitere Informationen finden Sie in der Android-Dokumentation zu SurfaceHolder und seinen beiden lockCanvas () -Methoden. developer.android.com/reference/android/view/… und developer.android.com/reference/android/view/…
UpLate

3

Für mich würde ein Anruf Canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)oder ähnliches erst funktionieren, wenn ich den Bildschirm berühre. Also würde ich die obige Codezeile aufrufen, aber der Bildschirm würde erst gelöscht, nachdem ich dann den Bildschirm berührt habe. Was für mich also funktionierte, war aufzurufen, invalidate()gefolgt von init()dem Aufruf zum Zeitpunkt der Erstellung, um die Ansicht zu initialisieren.

private void init() {
    setFocusable(true);
    setFocusableInTouchMode(true);
    setOnTouchListener(this);

    mPaint = new Paint();
    mPaint.setAntiAlias(true);
    mPaint.setDither(true);
    mPaint.setColor(Color.BLACK);
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setStrokeJoin(Paint.Join.ROUND);
    mPaint.setStrokeCap(Paint.Cap.ROUND);
    mPaint.setStrokeWidth(6);

    mCanvas = new Canvas();
    mPaths = new LinkedList<>();

    addNewPath();
}

3
canvas.drawColor(Color.TRANSPARENT, Mode.MULTIPLY);

2
Bitte fügen Sie eine Erklärung hinzu, wie Ihr Code das Problem löst. Dies ist als minderwertiger Beitrag gekennzeichnet - From Review
Suraj Rao

1

Das Löschen auf Canvas in Java Android ist ähnlich wie das Löschen von HTML Canvas über Javascript mit globalCompositeOperation. Die Logik war ähnlich.

U wählt die Logik DST_OUT (Destination Out).

paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));

Hinweis: DST_OUT ist nützlicher, da es 50% löschen kann, wenn die Lackfarbe 50% Alpha hat. Um vollständig bis transparent zu löschen, muss das Alpha der Farbe 100% betragen. Es wird empfohlen, paint.setColor (Color.WHITE) aufzutragen. Stellen Sie sicher, dass das Canvas-Bildformat RGBA_8888 war.

Kehren Sie nach dem Löschen mit SRC_OVER (Source Over) zur normalen Zeichnung zurück.

paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER));

Das Aktualisieren der Anzeige für kleine Bereiche muss buchstäblich auf Grafikhardware zugreifen und wird möglicherweise nicht unterstützt.

Am naheliegendsten für höchste Leistung ist die Verwendung einer Mehrbildebene.


Kannst du mir hier helfen? Ihre ist die einzige Lösung, die für meinen Fall geklappt hat, also habe ich abgestimmt. Es gibt nur ein kleines Problem, das gelöst werden muss
hamza khan

Ich verwende Leinwand mit Oberflächenhalter, also mache ich dies, um Leinwand (Leinwand aus Oberflächenhalter) zu löschen: Farbe !!. SetXfermode (PorterDuffXfermode (PorterDuff.Mode.DST_OUT)) Leinwand !!. DrawPaint (Farbe !!) OberflächeHalter! ! .unlockCanvasAndPost (Zeichenfläche) dann, um es neu zeichnen zu können: Farbe !!. setXfermode (PorterDuffXfermode (PorterDuff.Mode.SRC)) Zeichenfläche !!. drawPaint (Farbe !!) OberflächeHalter !!. UnlockCanvasAndPost (Zeichenfläche) jetzt das Problem Wenn ich nach dem Löschen der Leinwand eine Bitmap über die Leinwand zeichne, wird sie nach einem Farbblinken gezeichnet, was ärgerlich ist. Kannst du mir den Weg hierher zeigen? @phnghue
hamza khan

@hamza khan Hast du es mit SRC_OVER versucht? Weil SRC_OVER standardmäßig normal ist. Die SRC-Logik überschreibt alte Pixeldaten und ersetzt Alpha!
phnghue

0

Vergessen Sie nicht, invalidate () aufzurufen.

canvas.drawColor(backgroundColor);
invalidate();
path.reset();

Warum sollten Sie anrufen, invalidatenachdem Sie etwas gezeichnet haben?
RalfFriedl

0

Versuchen Sie, die Ansicht bei onPause () einer Aktivität zu entfernen und onRestart () hinzuzufügen.

LayoutYouAddedYourView.addView (YourCustomView); LayoutYouAddedYourView.removeView (YourCustomView);

Sobald Sie Ihre Ansicht hinzufügen, wird die onDraw () -Methode aufgerufen.

YourCustomView ist eine Klasse, die die View-Klasse erweitert.


0

In meinem Fall zeichne ich meine Leinwand in ein lineares Layout. So reinigen und neu zeichnen:

    LinearLayout linearLayout = findViewById(R.id.myCanvas);
    linearLayout.removeAllViews();

und dann rufe ich die Klasse mit den neuen Werten auf:

    Lienzo fondo = new Lienzo(this,items);
    linearLayout.addView(fondo);

Dies ist die Klasse Lienzo:

class Lienzo extends View {
    Paint paint;
    RectF contenedor;
    Path path;
    ArrayList<Items>elementos;

    public Lienzo(Context context,ArrayList<Items> elementos) {
        super(context);
        this.elementos=elementos;
        init();
    }

    private void init() {
        path=new Path();
        paint = new Paint();
        contenedor = new RectF();
        paint.setStyle(Paint.Style.FILL);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        contenedor.left = oneValue;
        contenedor.top = anotherValue;
        contenedor.right = anotherValue;
        contenedor.bottom = anotherValue;

        float angulo = -90; //starts drawing at 12 o'clock
        //total= sum of all element values
        for (int i=0;i<elementos.size();i++){
            if (elementos.get(i).angulo!=0 && elementos.get(i).visible){
                paint.setColor(elementos.get(i).backColor);
                canvas.drawArc(contenedor,angulo,(float)(elementos.get(i).value*360)/total,true,paint);

                angulo+=(float)(elementos.get(i).value*360)/total;
            }
        } //for example
    }
}

0

Mit dem folgenden Ansatz können Sie die gesamte Leinwand oder nur einen Teil davon löschen.
Bitte vergessen Sie nicht, die Hardwarebeschleunigung zu deaktivieren, da PorterDuff.Mode.CLEAR nicht mit der Hardwarebeschleunigung funktioniert und schließlich aufruft , setWillNotDraw(false)da wir die onDrawMethode überschreiben .

//view's constructor
setWillNotDraw(false);
setLayerType(LAYER_TYPE_SOFTWARE, null);

//view's onDraw
Paint TransparentPaint = new Paint();
TransparentPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
canvas.drawRect(0, 0, width, height, TransparentPaint); 

Es ist Arbeit, aber danach sind meine Draws nicht mehr sichtbar
Dyno Cris

1
@DynoCris Vielleicht verwenden Sie das gleiche Paint-Objekt!? Versuchen Sie es mit einem neuen und vergessen Sie nicht, bitte zu stimmen.
ucMedia

Es ist mein Code pastebin.com/GWBdtUP5. Wenn ich erneut versuche zu zeichnen, sehe ich keine unpassenden Linien. aber cansvas ist klar.
Dyno Cris

@DynoCris Bitte ändern Sie LAYER_TYPE_HARDWAREzuLAYER_TYPE_SOFTWARE
ucMedia vor

-1

Ihre erste Anforderung, wie Sie die gesamte Leinwand löschen oder neu zeichnen - Antwort - Verwenden Sie die Methode canvas.drawColor (color.Black), um den Bildschirm mit einer schwarzen Farbe oder einer von Ihnen angegebenen Farbe zu löschen.

Ihre zweite Anforderung, wie Sie einen Teil des Bildschirms aktualisieren - Antwort - zum Beispiel, wenn Sie alle anderen Dinge auf dem Bildschirm unverändert lassen möchten, aber in einem kleinen Bereich des Bildschirms, um eine Ganzzahl (z. B. Zähler) anzuzeigen, die alle fünf Sekunden zunimmt. Verwenden Sie dann die canvas.drawrect-Methode, um diesen kleinen Bereich zu zeichnen, indem Sie links oben rechts unten und malen angeben. Berechnen Sie dann Ihren Zählerwert (mit postdalayed für 5 Sekunden usw., ähnlich wie Handler.postDelayed (Runnable_Object, 5000);), konvertieren Sie ihn in eine Textzeichenfolge, berechnen Sie die x- und y-Koordinate in diesem kleinen Rechteck und verwenden Sie die Textansicht, um die Änderung anzuzeigen Zählerwert.


-1

Ich musste einen separaten Zeichnungsdurchlauf verwenden, um die Leinwand zu löschen (sperren, zeichnen und entsperren):

Canvas canvas = null;
try {
    canvas = holder.lockCanvas();
    if (canvas == null) {
        // exit drawing thread
        break;
    }
    canvas.drawColor(colorToClearFromCanvas, PorterDuff.Mode.CLEAR);
} finally {
    if (canvas != null) {
        holder.unlockCanvasAndPost(canvas);
    }
}

-3

Ruf einfach an

canvas.drawColor (Color.TRANSPARENT)


16
Das funktioniert nicht, weil es nur Transparenz über den aktuellen Clip zieht ... und praktisch nichts tut. Sie können jedoch den Porter-Duff-Übertragungsmodus ändern, um den gewünschten Effekt zu erzielen : Canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR). Wenn ich mich nicht irre, kann die Farbe tatsächlich alles sein (muss nicht TRANSPARENT sein), da PorterDuff.Mode.CLEARnur der aktuelle Clip gelöscht wird (wie das Stanzen eines Lochs in die Leinwand).
Ashughes
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.