【问题标题】:Canvas - zooming in, shifting, and scaling on AndroidCanvas - 在 Android 上放大、移动和缩放
【发布时间】:2020-12-13 00:16:19
【问题描述】:

我目前正在 webview 图像(下面的大象)上实现绘图功能。我在上面绘图没有问题,但缩放功能在绘图上做了一些时髦的东西(下面的第二张图片)。当我放大时,绘图不会放大而是移动。放大绘图也不起作用。我的代码如下:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.save();
    canvas.drawBitmap(canvasBitmap, 0, 0, canvasPaint);
    canvas.drawPath(drawPath, drawPaint);
    canvas.scale(mScaleFactor, mScaleFactor);
    canvas.restore();
    clipBounds = canvas.getClipBounds();
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    mScaleDetector.onTouchEvent(event);
    float touchX = event.getX() / mScaleFactor + clipBounds.left;
    float touchY = event.getY() / mScaleFactor + clipBounds.top;
    if (Deal.on)
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                drawPath.moveTo(touchX, touchY);
                break;
            case MotionEvent.ACTION_MOVE:
                drawPath.lineTo(touchX, touchY);
                break;
            case MotionEvent.ACTION_UP:
                drawCanvas.drawPath(drawPath, drawPaint);
                drawPath.reset();
                break;
            default:
                return false;
    }
    else {
        super.onTouchEvent(event);
    }
    invalidate();
    return true;
}

public class ScaleGestureListener extends SimpleOnScaleGestureListener {
    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        mScaleFactor *= detector.getScaleFactor();

        // Don't let the object get too small or too large.
        mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 5.0f));

        invalidate();

        return true;
    }
}

编辑:我设法使用 GestureDetector 的比例因子让画布通过缩放重新绘制。此外,我有一个从绘图切换到缩放/webview 控件的开关。我遇到的一个问题是双击 WebView 不会触发 onScale 手势。这意味着画布不会在放大时重新绘制和移动。

我需要实现一个功能来检测缩放因子受双击放大影响的程度。如果有人可以提出解决方案。这是更新代码:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    clipBounds = canvas.getClipBounds();
    canvas.save();
    drawPaint.setStrokeWidth(8/mScaleFactor);
    canvas.scale(mScaleFactor, mScaleFactor, 0, 0);
    canvas.drawPath(drawPath, drawPaint);
    canvas.drawBitmap(canvasBitmap, 0, 0, canvasPaint);
    canvas.restore();
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    pointerCount = event.getPointerCount();
    mScaleDetector.onTouchEvent(event);
    float touchX = (event.getX() + clipBounds.left) / mScaleFactor;
    float touchY = (event.getY() + clipBounds.top) / mScaleFactor;
    if (Deal.on){
        if (event.getPointerCount() > 1){
            return false;
        }
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                drawPath.moveTo(touchX, touchY);
                break;
            case MotionEvent.ACTION_MOVE:
                drawPath.lineTo(touchX, touchY);
                break;
            case MotionEvent.ACTION_UP:
                drawCanvas.drawPath(drawPath, drawPaint);
                drawPath.reset();
                break;
            default:
                return false;
        }
    }
    else {
        super.onTouchEvent(event);
    }
    invalidate();
    return true;
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   // Try for a width based on our minimum
   int minw = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth();
   int w = resolveSizeAndState(minw, widthMeasureSpec, 1);

   //int minh = MeasureSpec.getSize(w) - (int)mTextWidth + getPaddingBottom() + getPaddingTop();
   int h = resolveSizeAndState(MeasureSpec.getSize(w), heightMeasureSpec, 0);
   setMeasuredDimension(w, h);
}

public class ScaleGestureListener extends SimpleOnScaleGestureListener {

    @Override
    public boolean onScale(ScaleGestureDetector detector) {

        // Don't let the object get too small or too large.
        if(Deal.on == false) {
            mScaleFactor *= detector.getScaleFactor();
            mScaleFactor = Math.max(1f, Math.min(mScaleFactor, 20.0f));
        }

        invalidate();

        return true;
    }
}

【问题讨论】:

  • 在使用画布进行任何绘图之前,您是否尝试过缩放?
  • 问题是?为什么绘图没有缩放或为什么它不起作用?很明显,你每次都用同样的颜料画,为什么要放大呢?
  • 为什么要在 doubleTap 上进行缩放,这是在哪里实现的?如果这样做,您必须相应地更改 scaleFactor。
  • 如何相应地更改比例因子?除了 onScale 之外,还有其他实现的方法可以检测到我的 Gesture Listener 上的双倍变化吗?
  • 我双击放大的原因是因为我在 WebView 上绘图

标签: android android-canvas


【解决方案1】:

我已经实现了这种行为,但方式略有不同。我使用一个矩阵来处理所有的缩放和滚动(在我的例子中,还有旋转)。它可以生成整洁的代码,并且像发条一样工作。不过,我不知道是什么导致了你的古怪行为。

将一个矩阵和另一个路径存储为类成员:

Matrix drawMatrix = new Matrix();
Path transformedPath = new Path();

替换你的onScale:

@Override
public boolean onScale(ScaleGestureDetector detector) {
    Matrix transformationMatrix = new Matrix();

    //Zoom focus is where the fingers are centered, 
    transformationMatrix.postTranslate(-detector.getFocusX(), -detector.getFocusY());

    transformationMatrix.postScale(detector.getScaleFactor(), detector.getScaleFactor());

/* Using getFocuShift allows for scrolling with two pointers down. Remove it to skip this functionality */
    transformationMatrix.postTranslate(detector.getFocusX() + detector.getFocusShiftX(), detector.getFocusY() + detector.getFocusShiftY());

    drawMatrix.postConcat(transformationMatrix);
    invalidate();
    return true;
}

在 onDraw;跳过保存画布,而是:

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

    canvas.drawBitmap(canvasBitmap, drawMatrix, canvasPaint);
    transformedPath.rewind();
    transformedMatrix.addPath(drawPath);
    transformedPath.transform(drawMatrix, null);
    canvas.drawPath(transformedPath, drawPaint);
}

编码愉快!

【讨论】:

  • 实际上,这不会真正缩放路径。它将在放大的位置绘制,但线宽将与最初相同。这就是我这样做时想要的,但也许不是你想要的。
  • 线宽应随缩放比例缩放;如果放大它应该会变厚。
  • 嗯,不是很漂亮,不过你可以根据缩放改变lineWidth来达到这个效果。
  • 真正的问题是如何获得缩放?如果您双击 WebView,它会自动放大。有没有办法获得该缩放量?
【解决方案2】:

以非常简单的方式完成所有不同效果的好方法是使用画布方法

canvas.drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint);

此方法允许您获取整个源位图或只是其中的一个小样本 (Rect src),并根据需要投影它 (Rect dst),自动为您进行矩阵计算的缩放/平移,如图所示下面的例子:

这个例子只是缩放/缩放整个图像...

     Canvas canvas = null;
     Bitmap elephantBmp = null;
     Rect src = new Rect();
     Rect postRect = new Rect();
     Paint paintIfAny = new Paint();

     //To scale the whole image, first take the whole source and then postRect with a bigger size... 
     src.top = src.left = 0;
     src.right = elephantBmp.getWidth();
     src.bottom = elephantBmp.getHeight();
     //Here you have the chance to translate / scale the image(in this case i will not translate it but just scale it...)
     postRect.top = postRect.left = 0;
     postRect.right = (int)(elephantBmp.getWidth() * 1.1F);
     postRect.bottom = (int)(elephantBmp.getHeight() * 1.1F);

     canvas.drawBitmap(elephantBmp, src, postRect, paintIfAny);

这个例子只取了一个图像样本并显示了“内部缩放效果”

     Canvas canvas = null;
     Bitmap elephantBmp = null;
     Rect src = new Rect();
     Rect postRect = new Rect();
     Paint paintIfAny = new Paint();

    //To shift the whole image, first take the part of the original source image you want to show 
     src.top = topPosition;//Top coordinate of piece of image to show
     src.left = leftPosition;//Left coordinate of piece of image to show
     src.right = rightPosition;//Right coordinate of piece of image to show
     src.bottom = bottomPosition;//Bottom coordinate of piece of image to show
     //Here you have the chance to show it as big as you want...
     postRect.top = postRect.left = 0;
     postRect.right = (int)(src.width() * 1.1F);
     postRect.bottom = (int)(src.height() * 1.1F);

     canvas.drawBitmap(elephantBmp, src, postRect, paintIfAny);

通过使用这些 src/dst 对象,你几乎可以在 android 上做任何你想要的效果,是一个简单而强大的工具

希望这会有所帮助。

问候!

【讨论】:

    【解决方案3】:

    你能不能试着只画一次路径,让画布本身负责缩放

    private boolean pathsDrawn;
    
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        clipBounds = canvas.getClipBounds();
        canvas.save();
        drawPaint.setStrokeWidth(8/mScaleFactor);
        canvas.scale(mScaleFactor, mScaleFactor, 0, 0);
    
        if(!pathsDrawn) {
           canvas.drawPath(drawPath, drawPaint);
           pathsDrawn = true;
        }
        canvas.drawBitmap(canvasBitmap, 0, 0, canvasPaint);
        canvas.restore();
    }
    

    【讨论】:

    • 这只是在我松开手指时绘制路径。不影响缩放
    【解决方案4】:
    check below code will help you.
    
    //CUSTOM IMAGEVIEW
    import java.io.ByteArrayOutputStream;
    import android.annotation.SuppressLint;
    import android.content.Context;
    import android.graphics.Bitmap;
    import android.graphics.BitmapFactory;
    import android.graphics.Canvas;
    import android.graphics.Color;
    import android.graphics.Matrix;
    import android.graphics.Paint;
    import android.graphics.Path;
    import android.graphics.RectF;
    import android.graphics.drawable.Drawable;
    import android.util.AttributeSet;
    import android.util.FloatMath;
    import android.view.GestureDetector;
    import android.view.MotionEvent;
    import android.view.View;
    import android.view.View.OnTouchListener;
    import android.widget.ImageView;
    
    public class ScaleImageView extends ImageView implements OnTouchListener {
    
    static final float STROKE_WIDTH = 10f;
    static final float HALF_STROKE_WIDTH = STROKE_WIDTH / 2;
    
    float lastTouchX;
    float lastTouchY;
    final RectF dirtyRect = new RectF();
    
    private Context mContext;
    private float MAX_SCALE = 2f;
    
    private static Matrix mMatrix;
    private final float[] mMatrixValues = new float[9];
    
    // display width height.
    private int mWidth;
    private int mHeight;
    
    private int mIntrinsicWidth;
    private int mIntrinsicHeight;
    
    private float mScale;
    private float mMinScale;
    
    private float mPrevDistance;
    private boolean isScaling;
    
    private int mPrevMoveX;
    private int mPrevMoveY;
    private GestureDetector mDetector;
    
    Paint paint = new Paint();
    public static Path path = new Path();
    
    public static int imageheight, imagewidth;
    
    String TAG = "ScaleImageView";
    
    public ScaleImageView(Context context, AttributeSet attr) {
    super(context, attr);
    this.mContext = context;
    initialize();
    }
    
    public ScaleImageView(Context context) {
    super(context);
    this.mContext = context;
    initialize();
    }
    
    private void resetDirtyRect(float eventX, float eventY) {
    dirtyRect.left = Math.min(lastTouchX, eventX);
    dirtyRect.right = Math.max(lastTouchX, eventX);
    dirtyRect.top = Math.min(lastTouchY, eventY);
    dirtyRect.bottom = Math.max(lastTouchY, eventY);
    }
    
    @Override
    public void setImageBitmap(Bitmap bm) {
    super.setImageBitmap(bm);
    this.initialize();
    }
    
    @Override
    public void setImageResource(int resId) {
    super.setImageResource(resId);
    this.initialize();
    }
    
    private void initialize() {
    this.setScaleType(ScaleType.MATRIX);
    this.mMatrix = new Matrix();
    Drawable d = getDrawable();
    
    paint.setAntiAlias(true);
    paint.setColor(Color.RED);
    paint.setStyle(Paint.Style.STROKE);
    paint.setStrokeJoin(Paint.Join.ROUND);
    paint.setStrokeWidth(STROKE_WIDTH);
    
    if (d != null) {
        mIntrinsicWidth = d.getIntrinsicWidth();
        mIntrinsicHeight = d.getIntrinsicHeight();
        setOnTouchListener(this);
    }
    mDetector = new GestureDetector(mContext,
            new GestureDetector.SimpleOnGestureListener() {
                @Override
                public boolean onDoubleTap(MotionEvent e) {
                    maxZoomTo((int) e.getX(), (int) e.getY());
                    cutting();
                    return super.onDoubleTap(e);
                }
            });
    
    }
    
    @Override
    protected boolean setFrame(int l, int t, int r, int b) {
    mWidth = r - l;
    mHeight = b - t;
    
    mMatrix.reset();
    int r_norm = r - l;
    mScale = (float) r_norm / (float) mIntrinsicWidth;
    
    int paddingHeight = 0;
    int paddingWidth = 0;
    // scaling vertical
    if (mScale * mIntrinsicHeight > mHeight) {
        mScale = (float) mHeight / (float) mIntrinsicHeight;
        mMatrix.postScale(mScale, mScale);
        paddingWidth = (r - mWidth) / 2;
        paddingHeight = 0;
        // scaling horizontal
    } else {
        mMatrix.postScale(mScale, mScale);
        paddingHeight = (b - mHeight) / 2;
        paddingWidth = 0;
    }
    mMatrix.postTranslate(paddingWidth, paddingHeight);
    
    setImageMatrix(mMatrix);
    mMinScale = mScale;
    zoomTo(mScale, mWidth / 2, mHeight / 2);
    cutting();
    return super.setFrame(l, t, r, b);
    }
    
    protected float getValue(Matrix matrix, int whichValue) {
    matrix.getValues(mMatrixValues);
    return mMatrixValues[whichValue];
    }
    
    protected float getScale() {
    return getValue(mMatrix, Matrix.MSCALE_X);
    }
    
    public float getTranslateX() {
    return getValue(mMatrix, Matrix.MTRANS_X);
    }
    
    protected float getTranslateY() {
    return getValue(mMatrix, Matrix.MTRANS_Y);
    }
    
    protected void maxZoomTo(int x, int y) {
    if (mMinScale != getScale() && (getScale() - mMinScale) > 0.1f) {
        // threshold 0.1f
        float scale = mMinScale / getScale();
        zoomTo(scale, x, y);
    } else {
        float scale = MAX_SCALE / getScale();
        zoomTo(scale, x, y);
    }
    }
    
    public void zoomTo(float scale, int x, int y) {
    if (getScale() * scale < mMinScale) {
        return;
    }
    if (scale >= 1 && getScale() * scale > MAX_SCALE) {
        return;
    }
    mMatrix.postScale(scale, scale);
    // move to center
    mMatrix.postTranslate(-(mWidth * scale - mWidth) / 2,
            -(mHeight * scale - mHeight) / 2);
    
    // move x and y distance
    mMatrix.postTranslate(-(x - (mWidth / 2)) * scale, 0);
    mMatrix.postTranslate(0, -(y - (mHeight / 2)) * scale);
    setImageMatrix(mMatrix);
    }
    
    public void cutting() {
    int width = (int) (mIntrinsicWidth * getScale());
    int height = (int) (mIntrinsicHeight * getScale());
    
    imagewidth = width;
    imageheight = height;
    
    if (getTranslateX() < -(width - mWidth)) {
        mMatrix.postTranslate(-(getTranslateX() + width - mWidth), 0);
    }
    if (getTranslateX() > 0) {
        mMatrix.postTranslate(-getTranslateX(), 0);
    }
    if (getTranslateY() < -(height - mHeight)) {
        mMatrix.postTranslate(0, -(getTranslateY() + height - mHeight));
    }
    if (getTranslateY() > 0) {
        mMatrix.postTranslate(0, -getTranslateY());
    }
    if (width < mWidth) {
        mMatrix.postTranslate((mWidth - width) / 2, 0);
    }
    if (height < mHeight) {
        mMatrix.postTranslate(0, (mHeight - height) / 2);
    }
    setImageMatrix(mMatrix);
    }
    
    private float distance(float x0, float x1, float y0, float y1) {
    float x = x0 - x1;
    float y = y0 - y1;
    return FloatMath.sqrt(x * x + y * y);
    }
    
    private float dispDistance() {
    return FloatMath.sqrt(mWidth * mWidth + mHeight * mHeight);
    }
    
    public void clear() {
    path.reset();
    invalidate();
    }
    
    public static void save() {
    
    Bitmap returnedBitmap = Bitmap.createBitmap(
            ScaleImageViewActivity.imageview.getWidth(),
            ScaleImageViewActivity.imageview.getHeight(),
            Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(returnedBitmap);
    
    Drawable bgDrawable = ScaleImageViewActivity.imageview.getDrawable();
    
    if (bgDrawable != null)
        bgDrawable.draw(canvas);
    else
        canvas.drawColor(Color.WHITE);
    
    ScaleImageViewActivity.imageview.draw(canvas);
    
    ByteArrayOutputStream bs = new ByteArrayOutputStream();
    returnedBitmap.compress(Bitmap.CompressFormat.PNG, 50, bs);
    
    Bitmap FinalBitmap = BitmapFactory.decodeByteArray(bs.toByteArray(), 0,
            bs.toByteArray().length);
    
    ScaleImageViewActivity.imageview.setImageBitmap(FinalBitmap);
    path.reset();
    
    // ScaleImageViewActivity.imageview.setImageMatrix(mMatrix);
    
    }
    
    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent event) {
    if (!ScaleImageViewActivity.flag) {
    
        if (mDetector.onTouchEvent(event)) {
            return true;
        }
        int touchCount = event.getPointerCount();
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
        case MotionEvent.ACTION_POINTER_1_DOWN:
        case MotionEvent.ACTION_POINTER_2_DOWN:
            if (touchCount >= 2) {
                float distance = distance(event.getX(0), event.getX(1),
                        event.getY(0), event.getY(1));
                mPrevDistance = distance;
                isScaling = true;
            } else {
                mPrevMoveX = (int) event.getX();
                mPrevMoveY = (int) event.getY();
            }
        case MotionEvent.ACTION_MOVE:
            if (touchCount >= 2 && isScaling) {
                float dist = distance(event.getX(0), event.getX(1),
                        event.getY(0), event.getY(1));
                float scale = (dist - mPrevDistance) / dispDistance();
                mPrevDistance = dist;
                scale += 1;
                scale = scale * scale;
                zoomTo(scale, mWidth / 2, mHeight / 2);
                cutting();
            } else if (!isScaling) {
                int distanceX = mPrevMoveX - (int) event.getX();
                int distanceY = mPrevMoveY - (int) event.getY();
                mPrevMoveX = (int) event.getX();
                mPrevMoveY = (int) event.getY();
                mMatrix.postTranslate(-distanceX, -distanceY);
                cutting();
            }
            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_POINTER_UP:
        case MotionEvent.ACTION_POINTER_2_UP:
            if (event.getPointerCount() <= 1) {
                isScaling = false;
            }
            break;
        }
    } else {
        float eventX = event.getX();
        float eventY = event.getY();
    
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            path.moveTo(eventX, eventY);
            lastTouchX = eventX;
            lastTouchY = eventY;
            return true;
    
        case MotionEvent.ACTION_MOVE:
    
        case MotionEvent.ACTION_UP:
    
            resetDirtyRect(eventX, eventY);
            int historySize = event.getHistorySize();
            for (int i = 0; i < historySize; i++) {
                float historicalX = event.getHistoricalX(i);
                float historicalY = event.getHistoricalY(i);
                path.lineTo(historicalX, historicalY);
            }
            path.lineTo(eventX, eventY);
            break;
        }
    
        invalidate((int) (dirtyRect.left - HALF_STROKE_WIDTH),
                (int) (dirtyRect.top - HALF_STROKE_WIDTH),
                (int) (dirtyRect.right + HALF_STROKE_WIDTH),
                (int) (dirtyRect.bottom + HALF_STROKE_WIDTH));
    
        lastTouchX = eventX;
        lastTouchY = eventY;
    }
    return true;
    }
    
    @Override
    protected void onDraw(Canvas canvas) {
    // TODO Auto-generated method stub
    super.onDraw(canvas);
    if (ScaleImageViewActivity.flag)
        canvas.drawPath(path, paint);
    }
    
    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouch(View v, MotionEvent event) {
    return super.onTouchEvent(event);
    }
    
    }
    
    //ACTIVITY
    
    import android.app.Activity;
    import android.os.Bundle;
    import android.view.View;
    import android.view.View.OnClickListener;
    import android.widget.Button;
    
    public class ScaleImageViewActivity extends Activity implements OnClickListener {
    
    Button btndraw, btnzoom, btnsave;
    
    public static ScaleImageView imageview;
    public static boolean flag = true;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    initwidget();
    }
    
    private void initwidget() {
    
    imageview = (ScaleImageView) findViewById(R.id.image);
    
    btnsave = (Button) findViewById(R.id.activity_main_save);
    btndraw = (Button) findViewById(R.id.activity_main_zoom_draw);
    btnzoom = (Button) findViewById(R.id.activity_main_zoom_zoom);
    
    btndraw.setOnClickListener(this);
    btnzoom.setOnClickListener(this);
    btnsave.setOnClickListener(this);
    }
    
    @Override
    public void onClick(View arg0) {
    // TODO Auto-generated method stub
    if (btndraw.equals(arg0)) {
        flag = true;
    } else if (btnzoom.equals(arg0)) {
        flag = false;
    } else if (btnsave.equals(arg0)) {
        ScaleImageView.save();
    }
    }
    }
    main.xml
    
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >
    
    <LinearLayout
    android:layout_width="fill_parent"
    android:layout_height="wrap_content" >
    
    <Button
        android:id="@+id/activity_main_zoom_zoom"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Zoom" />
    
    <Button
        android:id="@+id/activity_main_zoom_draw"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Draw" />
    
    <Button
        android:id="@+id/activity_main_save"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Save" />
    </LinearLayout>
    
    <com.matabii.dev.scaleimageview.ScaleImageView
    android:id="@+id/image"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:src="@drawable/sample" />
    
    </LinearLayout>
    

    【讨论】:

      【解决方案5】:

      看看这个tutorial。这里作者使用两个 Matrix 对象来管理平移和缩放。我建议您使用矩阵,否则计算新坐标可能会很棘手且乏味。您的问题是由于没有正确管理翻译造成的。注意:在教程中,在 OnDraw() 内部,它错过了canvas.concat(matrix),把它放在canvas.drawBitmap(...) 之后

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2015-10-16
        • 1970-01-01
        • 2015-04-28
        • 2013-06-30
        • 2021-06-30
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多