Android 拖动 ImageView 很慢

Android drag ImageView very laggy

我正在尝试在 Android 应用程序中实现可触摸拖动 ImageView。在 SO 和其他地方有大量的示例代码,而且似乎都符合 this one.

我试过类似的东西,但它很慢,我一直收到警告

Choreographer﹕ Skipped XX frames!  The application may be doing too much work on its main thread.

我遵循 the Android documentation advice 并尝试在单独的线程中完成这项工作,但这并没有解决问题。

基本上我在我的视图中注册了一个 OnTouchListener,然后在 onTouch() 方法中我启动了一个新线程来尝试完成工作(布局参数的最终更新被发送回UI 线程使用 View.post() 进行处理)。

我看不出如何通过在主线程上做更少的工作来实现这一点。有什么方法可以修复我的代码吗?或者是否有更好的方法?

代码:

    private void setTouchListener() {
        final ImageView inSituArt = (ImageView)findViewById(R.id.inSitu2Art);
        inSituArt.setOnTouchListener(new View.OnTouchListener() {

            @Override
            public boolean onTouch(View v, MotionEvent event) {

                final MotionEvent event2 = event;

                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) inSituArt.getLayoutParams();
                        switch (event2.getAction()) {
                            case MotionEvent.ACTION_DOWN: {
                                int x_cord = (int) event2.getRawX();
                                int y_cord = (int) event2.getRawY();
                                // Remember where we started
                                mLastTouchY = y_cord;
                                mLastTouchX = x_cord;
                            }
                            break;
                            case MotionEvent.ACTION_MOVE: {
                                int x_cord = (int) event2.getRawX();
                                int y_cord = (int) event2.getRawY();
                                float dx = x_cord - mLastTouchX;
                                float dy = y_cord - mLastTouchY;

                                layoutParams.leftMargin += dx;
                                layoutParams.topMargin += dy;
                                layoutParams.rightMargin -= dx;


                                final FrameLayout.LayoutParams finalLayoutParams = layoutParams;
                                inSituArt.post(new Runnable() {
                                    @Override
                                    public void run() {
                                        inSituArt.setLayoutParams(finalLayoutParams);
                                    }
                                });


                                mLastTouchX = x_cord;
                                mLastTouchY = y_cord;
                            }
                            break;
                            default:
                                break;
                        }

                    }
                }).start();
                return true;
            }
        });
    }

onTouch 是一个重复触发的事件,每次触发 onTouch 时,您都要求视图在新位置重新绘制自身。绘制对于您的应用程序来说是一项非常昂贵的操作 - 永远不应该要求它在常规视图层次结构中重复执行此操作。您的目标应该是减少出现的绘图命令的数量,因此我将设置一个简单的模运算,只允许它每隔 10 onTouch 个事件执行一次移动。您最终会在 看起来 断断续续的地方进行权衡,因为如果您将模数设置得太高,它不会在每一帧发生,但应该在 5-30 范围内保持平衡。

在您的 class 上方,您需要如下内容:

static final int REFRESH_RATE = 10; //or 20, or 5, or 30, whatever works best

int counter = 0;

然后在你的触摸事件中:

case MotionEvent.ACTION_MOVE: {
    counter += 1;
    if((REFRESH_RATE % counter) == 0){ //this is the iffstatement you are looking for
      int x_cord = (int) event2.getRawX();
      int y_cord = (int) event2.getRawY();
      float dx = x_cord - mLastTouchX;
      float dy = y_cord - mLastTouchY;
      layoutParams.leftMargin += dx;
      layoutParams.topMargin += dy;
      layoutParams.rightMargin -= dx;

      final FrameLayout.LayoutParams finalLayoutParams = layoutParams;
      inSituArt.post(new Runnable() {
        @Override
        public void run() {
           inSituArt.setLayoutParams(finalLayoutParams);
        }
      });

      mLastTouchX = x_cord;
      mLastTouchY = y_cord;
    }
    counter = 0;
}

new Thread 中,您刚刚为拖动运动添加了更多的波动。 正如@JohnWhite 所提到的,onTouch 被连续调用了很多次,像这样创建和触发新线程真的非常糟糕。

您一开始遇到这些问题的一般原因是因为您遵循的示例代码虽然有效,但它是一个糟糕的未优化代码。

每次您调用 setLayoutParams 系统都必须执行一个新的完整布局过程,而且需要连续做很多事情。

我不会完全为您编写代码,但我会指出可能的替代方案。

首先删除你使用的所有线程逻辑。你并没有真正执行任何复杂的计算,一切都可以在 UI-线程上 运行。

  1. 使用 offsetTopAndBottom and offsetLeftAndRight 在屏幕上移动您的 ImageView。
  2. 将 ViewTransformations 与 setTranslationX(和 Y)一起使用
  3. 将您的 ImageView 更改为 match_parent 并创建自定义 ImageView。在此自定义视图中,您将覆盖 onDraw 方法以在绘制图像之前移动图像。像这样:

     @Override
     public void onDraw(Canvas canvas) {
       int saveCount = canvas.save();
       canvas.translate(dx, dy); // those are the values calculated by your OnTouchListener
       super.onDraw(canvas); // let the image view do the normal draw
       canvas.restoreToCount(saveCount); // restore the canvas position
     }
    

最后一个选项非常有趣,因为它绝对不会改变布局。它只是让 ImageView 处理整个区域并将绘图移动到正确的位置。非常有效的做法。

对于其中一些选项,您需要在视图中调用 "invalidate",以便可以再次调用 draw

找了半天终于找到了真正的解决方法

您应该使用 Drawable 而不是 Bitmap。

override fun onDraw(canvas: Canvas?) {
        canvas?.save()
        canvas?.concat(myCustomMatrix)
        mDrawable?.draw(canvas!!)
        canvas?.restore()
    }

移动图像(dx, dy)

myCustomMatrix.postTranslate(dx, dy)
invalidate()