在 Android 中平滑滚动画布

Posted

技术标签:

【中文标题】在 Android 中平滑滚动画布【英文标题】:Scrolling a Canvas smoothly in Android 【发布时间】:2011-01-05 23:09:41 【问题描述】:

我是 android 新手。

我在视图的 OnDraw(Canvas canvas) 方法内的 Canvas 上绘制位图、线条和形状。我正在寻求有关如何实现平滑滚动以响应用户拖动的帮助。我已经搜索但没有找到任何教程来帮助我。

Canvas 的参考似乎是说,如果 Canvas 是由 Bitmap(例如称为 bmpBuffer)构造的,那么在 Canvas 上绘制的任何内容也会在 bmpBuffer 上绘制。是否可以使用 bmpBuffer 来实现滚动...也许将其复制回 Canvas 一次移动几个像素?但是如果我使用 Canvas.drawBitmap 将 bmpBuffer 绘制回移动了几个像素的 Canvas,bmpBuffer 不会被破坏吗?因此,也许我应该将 bmpBuffer 复制到 bmpBuffer2,然后将 bmpBuffer2 绘制回 Canvas。

一种更直接的方法是将线条、形状等直接绘制到缓冲区位图中,然后将该缓冲区(带有移位)绘制到画布上,但据我所知,有各种方法:drawLine()、 drawShape() 等不可用于绘制到位图……只能绘制到画布上。

我可以有 2 个画布吗?其中一个将从缓冲区位图构造并仅用于绘制线条、形状等,然后将缓冲区位图绘制到另一个 Canvas 上以在视图中显示?

我应该欢迎任何建议!

此处(以及其他网站上)类似问题的答案请参阅“blitting”。我理解这个概念,但在 Android 文档中找不到任何关于“blit”或“bitblt”的信息。 Canvas.drawBitmap 和 Bitmap.Copy 是 Android 的等价物吗?

【问题讨论】:

今天早上做了更多的谷歌搜索。根据这个网页markmail.org/message/oedvjxi3dhokzq23我可以有第二个画布,所以我会探索这个想法。 在下面的新“答案”中查看更多信息。 【参考方案1】:

我似乎找到了答案。我已将大部分绘图代码(以前在 onDraw() 中)放在一个新的 doDrawing() 方法中。此方法首先创建一个比屏幕大的新位图(大到足以容纳完整的绘图)。然后它会创建第二个 Canvas 来进行详细绘图:

    BufferBitmap = Bitmap.createBitmap(1000, 1000, Bitmap.Config.ARGB_8888);
    Canvas BufferCanvas = new Canvas(BufferBitmap);

doDrawing() 方法的其余部分用于详细绘制到 BufferCanvas。

整个 onDraw() 方法现在如下所示:

    @Override protected void onDraw(Canvas canvas) 
    super.onDraw(canvas);
    canvas.drawBitmap(BufferBitmap, (float) -posX, (float) -posY, null);

位置变量 posX 和 posY 在应用程序的 onCreate() 方法中初始化为 0。应用程序实现 OnGestureListener 并使用 OnScroll 通知中返回的 distanceX 和 distanceY 参数来增加 posX 和 posY。

这似乎是实现平滑滚动所需的全部内容。还是我忽略了什么!?

【讨论】:

自从写下这个“答案”后我发现的一件事是,当屏幕旋转时,需要额外的代码来“回收”缓冲区位图(和任何其他位图对象)。这是因为当屏幕旋转时,应用程序的主要活动结束并重新启动,但系统不会自动恢复位图对象使用的内存。因此,当不再需要时,为每个位图调用 recycle() 方法似乎很重要。对于缓冲区位图,答案似乎是覆盖 Activity 的 onDestroy 方法并在其中调用位图的 recycle()。 只需将android:configChanges="orientation" 放入Manifest 即可避免此代码。请参阅 Ribo 于 2011 年 1 月 4 日对此问题的回答。 请帮忙解决这个问题***.com/questions/11720702/canvas-zoom-in-partially/… 抱歉,kamal 和 WildBill,响应缓慢。我最近没有看这个线程。我很高兴你解决了你的问题,卡马尔。 WildBill,在创建视图后,我首先从主 Activity 的 onCreate() 调用 doDrawing();每当需要更改缓冲区位图中的图像时,我都会再次调用 doDrawing()。【参考方案2】:

我也遇到过这个问题

我是这样画的:

Canvas BigCanvas = new Canvas();
Bitmap BigBitmap = new Bitmap(width,height);

int ScrollPosX , ScrollPosY  // (calculate these with the onScrollEvent handler)

void onCreate()

   BigCanvas.SetBitmap(BigBitmap);


onDraw(Canvas TargetCanvas)

   // do drawing stuff
   // ie.  BigCanvas.Draw.... line/bitmap/anything

   //draw to the screen with the scrolloffset

   //drawBitmap (Bitmap bitmap, Rect src, Rect dst, Paint paint)
   TargetCanvas.DrawBitmap(BigBitmap(new Rect(ScrollPosX,ScrollPosY,ScrollPosX + BigBitmap.getWidth(),ScrollPosY + BigBitmap.getHeight(),new Rect(0,0,ScreenWidth,ScreenHeight),null);

为了平滑滚动,您需要制作某种方法,在滚动后需要几个点(即第一个滚动点和第 10 个),减去这些并在每个循环中滚动该数字,使其逐渐较慢(ScrollAmount - 转数 - 摩擦)。

我希望这能提供更多见解。

【讨论】:

感谢默文,您的回复。我不确定我是否完全理解你的滚动是如何工作的。您是否等到收到 10 个滚动事件后才开始移动位图?我曾想象过,为了让屏幕显示感觉完全响应,我需要在收到第一个滚动事件后立即将位图重绘到 Canvas 上。而且,因为我的“绘图内容”非常耗时,所以我决定将其移出 onDraw 方法,以使程序尽可能响应滚动。【参考方案3】:

继续回复维克多……

其实情况更复杂。因为 doDrawing 过程非常缓慢(在我的旧 HTC Hero 手机上需要 2-3 秒),我发现最好弹出一条 Toast 消息来告知用户它正在发生并指出原因。显而易见的方法是创建一个只包含 2 行的新方法:

public void redrawBuffer(String strReason) 
    Toaster.Toast(strReason, "Short");`
    doDrawing();

并从我程序中的其他地方调用此方法,而不是 doDrawing()。

但是,我发现烤面包机要么从未出现,要么闪现过短以至于无法读取。我的解决方法是使用时间检查处理程序来强制程序在显示 Toast 和调用 doDrawing() 之间休眠 200 毫秒。尽管这会稍微延迟重绘的开始,但我认为就程序的可用性而言,这是值得付出的代价,因为用户知道发生了什么。 reDrawBuffer() 现在读取:

public void redrawBuffer(String strReason) 
    Toaster.Toast(strReason, "Short");
    mTimeCheckHandler.sleep(200);    
`

处理程序代码(嵌套在我的 View 类中)是:

private timeCheckHandler mTimeCheckHandler = new timeCheckHandler();

class timeCheckHandler extends Handler 
@Override
    public void handleMessage(Message msg) 
        doDrawing();
    
    public void sleep(long delayMillis) 
        this.removeMessages(0);
        sendMessageDelayed(obtainMessage(0), delayMillis);
    
`

【讨论】:

【参考方案4】:

无需重新启动活动! (根据 prepbgg 的 Jan 27 10 对他 Jan 17 10 'answer' 的回复),您可以通过放置如下所示的 'android:configChanges' 属性来避免加载应用程序,而不是回收位图并产生重新加载活动的开销,在应用程序的 AndroidManifest.xml 文件的“活动”元素中。这告诉系统应用程序将处理方向更改并且不需要重新启动应用程序。

<activity android:name=".ANote"
    android:label="@string/app_name"
    android:configChanges="orientation|screenLayout">
    <intent-filter>
       <action android:name="android.intent.action.MAIN" />
       <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

此方法可用于在方向更改时获取通知:

public void onConfigurationChanged(Configuration  newConfig) 
    super.onConfigurationChanged(newConfig);
    prt("onConfigurationChanged: "+newConfig);

    if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) 
      prt("  PORTRAIT");
     else 
      prt("  LANDSCAPE");
    
 // end of onConfigurationChanged

【讨论】:

这很有趣。 (事实上​​,我确实在相当早的阶段通过将 android:screenOrientation="portrait" 放入 Manifest 来阻止应用程序更改方向。我这样做是为了解决内存问题。)但是,我想处理旋转如果可能的。当您谈到应用程序处理方向更改时,我需要做些什么来实现这一点? View 的 onDraw 方法还会被自动调用吗?还是我只是简单地通过“无效”视图来响应旋转? 我刚刚将 Manifest 中的 android:screenOrientation="portrait" 替换为 android:configChanges="orientation" 并且该应用程序正常运行! (不需要任何代码来检查方向变化......屏幕会在我没有任何干预的情况下重新绘制。)LogCat 建议手机在每次方向变化时花费大约 250 毫秒的垃圾收集时间。我还看不出有什么问题。非常感谢您带着这个建议来到这里。【参考方案5】:

prepbgg:我认为代码不会起作用,因为 canvas.drawBitmap 不会绘制到位图中,而是将位图绘制到画布上。

如果我错了,请纠正我!

【讨论】:

谢谢,MasterGaurav。我同意 canvas.drawBitmap(BufferBitmap, ...) 不会在 BufferBitmap 中绘制任何内容。但是,我有一个单独的 doDrawing() 方法,其中事物(线条、位图等)被绘制到 BufferCanvas 中,从而被绘制到 BufferBitmap 中。该代码似乎确实有效(尽管它偶尔会卡顿和/或崩溃......我不知道这是因为我的绘图代码有问题还是有其他错误。)

以上是关于在 Android 中平滑滚动画布的主要内容,如果未能解决你的问题,请参考以下文章

如何在纯 JavaScript 中平滑滚动到元素

css 在jQuery中平滑页面滚动

如何在 Java 中平滑 JFrame 的滚动

在纯 CSS 视差中平滑滚动

在 XCode 中平滑滚动视图和表格视图之间的滚动过渡

在 UITableview 中平滑滚动以显示聊天记录