如何绘制具有可变笔画宽度的路径

Posted

技术标签:

【中文标题】如何绘制具有可变笔画宽度的路径【英文标题】:How to draw a path with variable stroke width 【发布时间】:2013-03-08 05:29:24 【问题描述】:

我的代码基本上来自此示例 (http://corner.squareup.com/2010/07/smooth-signatures.html) 和 Google API (FingerPaint),但现在我想使用 VelocityTracker 类来根据手指的速度更改笔画宽度。

我认为我可以将路径分成更小的部分,但我没有找到任何示例。还有第二篇文章 (http://corner.squareup.com/2012/07/smoother-signatures.html),但我既没有特定的贝塞尔曲线类,也没有收集 ArrayList 中的所有点,因此他们调整笔画宽度的示例不是很有帮助。

有人知道如何处理这个问题吗?我两周前开始学习代码,所以我对所有这些东西都很陌生。

编辑:我尝试实现 MotionEvents 的速度,并在运行应用程序时使用 LogCat 跟踪当前速度。它确实奏效了,但是当我尝试将速度用作 mPaint.setStrokeWidth 参数的一部分时,我没有得到我真正想要的。我在画布上绘制的路径的宽度从我开始画线到我向上移动手指的那一刻一直在变化。所以这就是为什么我想将路径分成更小的部分,因为现在只有最后跟踪的速度会影响strokeWidth。

public class SignatureView extends View 

    private static final String TAG = SignatureView.class.getSimpleName();
    private static final float STROKE_WIDTH = 10;
    private static final float HALF_STROKE_WIDTH = STROKE_WIDTH / 2;
    private final double TOUCH_TOLERANCE = 5;
    private int h = getResources().getDisplayMetrics().heightPixels;
    private int w = getResources().getDisplayMetrics().widthPixels;

    private Path mPath = new Path();
    private Paint mPaint = new Paint();
    private Paint mBitmapPaint = new Paint(Paint.DITHER_FLAG);
    private Bitmap mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
    private Canvas mCanvas = new Canvas(mBitmap);

    private float mX, mY;
    private float lastTouchX, lastTouchY;
    private final RectF dirtyRect = new RectF();

public SignatureView(Context context, AttributeSet attrs) 
    super(context, attrs);

    mPaint.setAntiAlias(true);
    mPaint.setColor(Color.BLACK);
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setStrokeJoin(Paint.Join.ROUND);
    mPaint.setStrokeWidth(INITIAL_STROKE_WIDTH);

    Log.d(TAG, "TOUCH_TOLERANCE = " +TOUCH_TOLERANCE);


@Override
protected void onDraw(Canvas canvas) 
    canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);            
    canvas.drawPath(mPath, mPaint);


@Override
public boolean onTouchEvent(MotionEvent event) 
    float eventX = event.getX();
    float eventY = event.getY();
    int historySize = event.getHistorySize();

    switch (event.getAction()) 

        case MotionEvent.ACTION_DOWN:

            resetDirtyRect(eventX, eventY);
            mPath.reset();
            mPath.moveTo(eventX, eventY);
            mX = eventX;
            mY = eventY;
            break;

        case MotionEvent.ACTION_MOVE:

            float dx = Math.abs(eventX - mX);
            float dy = Math.abs(eventY - mY);

            if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) 

                mPath.quadTo(mX, mY, (eventX + mX)/2, (eventY + mY)/2);
                mX = eventX;
                mY = eventY;
            

            for (int i = 0; i < historySize; i++) 
                float historicalX = event.getHistoricalX(i);
                float historicalY = event.getHistoricalY(i);
                expandDirtyRect(historicalX, historicalY);
            
            break;

        case MotionEvent.ACTION_UP:

            for (int i = 0; i < historySize; i++) 
                float historicalX = event.getHistoricalX(i);
                float historicalY = event.getHistoricalY(i);
                expandDirtyRect(historicalX, historicalY);
            

            mPath.lineTo(mX, mY);
            mCanvas.drawPath(mPath, mPaint);
            mPath.reset();
            break;

        default:
            Log.d(TAG, "Ignored touch event: " + event.toString());
        return false;
    

    // Include half the stroke width to avoid clipping.
        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;


private void expandDirtyRect(float historicalX, float historicalY) 
    if (historicalX < dirtyRect.left) 
        dirtyRect.left = historicalX;
     else if (historicalX > dirtyRect.right) 
        dirtyRect.right = historicalX;
    
    if (historicalY < dirtyRect.top) 
        dirtyRect.top = historicalY;
     else if (historicalY > dirtyRect.bottom) 
        dirtyRect.bottom = historicalY;
    


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);


【问题讨论】:

你有没有设法根据我手指的速度改变笔划宽度。实际上,我的代码中也遇到了同样的问题,但还没有解决。 【参考方案1】:

您可以在每次行程值根据速度变化时使用拆分路径对象。在你SignatureView类添加

private Path mPath = new Path();
ArrayList<Path> mPaths = new ArrayList<Path>();

并取另一个 ArrayList 来保存每个路径的笔画值

ArrayList<int> strokes = new ArrayList<int>();

添加变量lastStroke 以及lastTouchXlastTouchY。我会建议你将lastStroke 设为int 类型。

private int lastStroke = -1; //give an initial value

现在你的onTouchEvent 方法应该是这样的

@Override
public boolean onTouchEvent(MotionEvent event) 
    float eventX = event.getX();
    float eventY = event.getY();
    int historySize = event.getHistorySize();
    int eventStroke= //calculate stroke size with velocity and make it between 1-10 or any range you seem fit

    switch (event.getAction()) 

        case MotionEvent.ACTION_DOWN:

            resetDirtyRect(eventX, eventY);
            mPath.reset();
            mPath.moveTo(eventX, eventY);
            mX = eventX;
            mY = eventY;
            break;

        case MotionEvent.ACTION_MOVE:

            float dx = Math.abs(eventX - mX);
            float dy = Math.abs(eventY - mY);

            if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) 
                if(lastStroke != evetnStroke)
                    mPath = new Path();
                    mPath.moveTo(mX,mY);
                    mPaths.Add(mPath);
                    mStrokes.Add(eventStroke);
                
                mPath.quadTo(mX, mY, (eventX + mX)/2, (eventY + mY)/2);
                mX = eventX;
                mY = eventY;
            

            for (int i = 0; i < historySize; i++) 
                float historicalX = event.getHistoricalX(i);
                float historicalY = event.getHistoricalY(i);
                expandDirtyRect(historicalX, historicalY);
            
            break;

        case MotionEvent.ACTION_UP:

            for (int i = 0; i < historySize; i++) 
                float historicalX = event.getHistoricalX(i);
                float historicalY = event.getHistoricalY(i);
                expandDirtyRect(historicalX, historicalY);
            

           mPath.lineTo(mX, mY);
           break;

        default:
            Log.d(TAG, "Ignored touch event: " + event.toString());
        return false;
    
    // Include half the stroke width to avoid clipping.
    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;
    lastStroke = eventStroke;
    return true;

你的 ondraw 方法是

@Override
protected void onDraw(Canvas canvas) 
    for(int i=0; i<mPaths.size();i++)
        mPaint.setStrokeWidth(strokes.get(i));
        canvas.drawPath(mPaths.get(i), mPaint);
    

这是基本思想。您需要对其进行修改以使其正常工作。

【讨论】:

嘿,谢谢你的回答!我认为现在我在正确的轨道上,但仍然存在问题。我想这与“太快”的 android 缓冲接触点有关。我试图让它们使用类似于用于扩展dirtyRect的循环的循环。但我总是有路径之间的差距。我想投票给你,但我的代表太低了,对不起! 我遇到了同样的问题。然后我修改了代码,使新路径总是从上一条路径的最后一点开始。例如,如果 (x,y) 是一条路径的最后一个点。然后下一条路径将首先移动到 (x,y)。在 android 术语中'path.movrTo(x,y)' @Dave 你有没有顺利地画出你的线......实际上我面临同样的问题,你能看看我在*** ***.com/questions/20560322/…发布的这个问题 @th1rdey3 如何获取 eventStroke 值。 需要计算笔画,笔画没有事件。如果您使用手写笔输入,则可以获取压力事件值并使用它来计算笔画大小。但是对于手指触摸,压力事件总是返回值1

以上是关于如何绘制具有可变笔画宽度的路径的主要内容,如果未能解决你的问题,请参考以下文章

如何在具有可变宽度 Unicode 字符的 Swift 字符串上使用 NSRegularExpression?

打开具有可变宽度的关闭侧菜单(在 xcode 中)

如何使用 AutoLayout 配置视图宽度固定宽度和可变高度

scss 带有截断的可变列宽,当其中一列具有可变宽度时。使用flexbox

使 NSView 具有适合内容的固定宽度和可变高度

具有可变宽度的jQuery触摸滑块插件