在Android中将文本包裹在贝塞尔曲线外部

16

我想在Android中将文本包裹在贝塞尔曲线形状之外。

我尝试过的:

Path path = new Path();
path.addCircle(x, y, radius, Path.Direction.CW);
myCanvas.drawTextOnPath(myText, path, offset, 0, myPaint);

我想要实现的目标:

但是这段代码在曲线上绘制文本..而我不想在曲线上写文本..我希望根据曲线换行并将其写在下一行。

为了更清楚地理解,请参考baconforme.com..我想在Android上创建这种类似于jquery的行为,而不使用Web浏览器。

并且看到了这个链接:On Android how do I wrapping text inside in a bezier path

问题:

  1. 这是否可行?
  2. 如果是,请指导我。
1个回答

9
我已经实现了一个基本视图,可以执行您要做的操作。这里的想法是根据您请求的路径创建位图。路径外的每个像素都将具有0值,路径内的每个像素都将具有其他值。
通过这样做,您可以知道某一点是否在多边形内部。现在,我们需要确定文本的绘制位置。
我将通过遍历生成的位图来生成矩形列表。每个矩形将定义在多边形内部开始和结束的行。
对于每个矩形,我开始填充文本,直到该矩形无法容纳更多文本,在这种情况下,我移动到下一个矩形。一旦没有更多的矩形,或者我用完了文本,我就停止绘制。
在此实现中,我添加了一些自定义选项,例如字体大小、文本颜色和换行模式。
这就是代码:PolygonWrapView.java
public class PolygonWrapView extends View
{
    public enum WrapMode
    {
        Letters,
        Words
    }

    private Path mPath;
    private String mText;
    private float mFontSize;
    private int mTextColor;

    private Paint mPaint;
    private Bitmap mPathMap;

    private WrapMode mWrapMode = WrapMode.Words;

    public PolygonWrapView(Context context)
    {
        super(context);
        init();
    }

    public PolygonWrapView(Context context, AttributeSet attrs)
    {
        super(context, attrs);
        init();
    }

    public PolygonWrapView(Context context, AttributeSet attrs, int defStyleAttr)
    {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init()
    {
        mPaint = new Paint();
        mFontSize = 20;
        mTextColor = 0xFF000000;
    }

    public void setPath(Path path)
    {
        mPath = path;

        // invalidate the path map.
        mPathMap = null;
    }

    // This method converts the path into a bitmap which will be used to determine if a point is within the path
    private void generatePathMap()
    {
        if (mPath != null)
        {
            // the path map bitmap can have poor quality, we're only checking for color or no color in each pixel.
            mPathMap = Bitmap.createBitmap(getMeasuredWidth(), getMeasuredHeight(), Bitmap.Config.ARGB_4444);

            Canvas canvas = new Canvas(mPathMap);

            Paint pathPaint = new Paint();
            pathPaint.setStyle(Paint.Style.FILL);
            pathPaint.setColor(0xFFFFFFFF);

            // draw the path.
            canvas.drawPath(mPath, pathPaint);
        }
    }

    public void setText(String text)
    {
        mText = text;
    }

    public void setFontSize(float fontSize)
    {
        mFontSize = fontSize;
    }

    public void setTextColor(int textColor)
    {
        mTextColor = textColor;
    }

    public void setWrapMode(WrapMode wrapMode)
    {
        mWrapMode = wrapMode;
    }

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

        // make sure we have enough data to begin drawing text.
        if (mPath == null || mText == null || getMeasuredWidth() == 0 || getMeasuredHeight() == 0)
            return;

        // if the path map hasn't been generated, generate it now.
        if (mPathMap == null)
            generatePathMap();

        final List<Rect> writableRects = getTextRects();
        final List<String> textFragments = getTextFragments();

        mPaint.setColor(mTextColor);
        mPaint.setTextSize(mFontSize);

        int rectIndex = 0;
        int fragmentIndex = 0;
        Rect rect = null;
        String textFragment = null;
        float textWidth;

        // maybe find a better way to limit this loop?
        while (true)
        {
            // we don't have a rectangle. Get the next 1 in the list.
            if (rect == null)
            {
                // no more rectangles to draw text on. Finish.
                if (rectIndex >= writableRects.size())
                    return;

                rect = new Rect(writableRects.get(rectIndex));
                rectIndex++;
            }

            // we don't have text to print. Get the next word in the list.
            if (textFragment == null)
            {
                // no more text to draw. Finish.
                if (fragmentIndex >= textFragments.size())
                    return;

                textFragment = textFragments.get(fragmentIndex);
                fragmentIndex++;
            }

            // find how much width this text wants.
            textWidth = mPaint.measureText(textFragment);

            // if the rectangle doesn't have enough width, set it to null, indicating its "used up" and we need to next rect. Don't continue drawing text, find a new rect first.
            if (textWidth > rect.width())
            {
                rect = null;
                continue;
            }

            // draw the text.
            canvas.drawText(textFragment, rect.left, rect.centerY(), mPaint);

            // the word has been drawn. Set it null indicating a new 1 is needed in the next iteration.
            textFragment = null;

            // remove the used width from the rect and continue.
            rect.left += textWidth;

            // In word mode, account for the space that was removed.
            if (mWrapMode == WrapMode.Words)
            {
                rect.left += mPaint.measureText(" ");
            }
        }
    }

    // get each String fragment as a list. For letters mode, each element will be a letter or char. For words mode, each element will be a word.
    private List<String> getTextFragments()
    {
        List<String> result = new ArrayList<String>();

        if (mWrapMode == WrapMode.Letters)
        {
            for (int i = 0; i < mText.length(); i++)
            {
                result.add("" + mText.charAt(i));
            }
        }
        else if (mWrapMode == WrapMode.Words)
        {
            String[] words = mText.split("\\s+");

            for (String word : words)
                result.add(word);
        }


        return result;
    }

    private List<Rect> getTextRects()
    {
        final List<Rect> result = new ArrayList<Rect>();

        boolean isInPolygon = false;
        Rect rect = null;

        // place y in the center of the text, jump in fontsize steps.
        for (int y = (int)(mFontSize / 2); y < getMeasuredHeight(); y += mFontSize)
        {
            // place x at 0, jump with 5 px steps. This can be adjusted for better accuracy / performance.
            for (int x = 0; x < getMeasuredWidth(); x += 5)
            {
                // Havent found a point within the polygon yet, but now I have!
                if (!isInPolygon && mPathMap.getPixel(x, y) != 0)
                {
                    isInPolygon = true;
                    rect = new Rect(x, y - (int)(mFontSize / 2), x, y + (int)(mFontSize / 2));
                }
                // We found where the polygon started in this row, and now we found where it ends.
                else if (isInPolygon && mPathMap.getPixel(x, y ) == 0)
                {
                    isInPolygon = false;
                    rect.right = x;

                    result.add(rect);
                }
            }

            // If the edge is in the ploygon, limit the rect to the right side of the view.
            if (isInPolygon)
            {
                rect.right = getMeasuredWidth();
                result.add(rect);
            }
        }

        return result;
    }
}

activity_main.xml:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:tools="http://schemas.android.com/tools"
                android:layout_width="match_parent"
                android:layout_height="match_parent">


    <com.gil.polygonwrap.PolygonWrapView
        android:id="@+id/polygonWrap"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</RelativeLayout>

MainActivity.java:(使用示例)

public class MainActivity extends ActionBarActivity
{
    private PolygonWrapView mPolygonWrapView;

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mPolygonWrapView = (PolygonWrapView)findViewById(R.id.polygonWrap);

        final String text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";

        mPolygonWrapView.setText(text);

        Path path = new Path();

        // sample of adding a path with a bezier curve element
        path.moveTo(0, 0);
        path.lineTo(500, 0);
        path.cubicTo(700, 300, 400, 600, 800, 1000);
        path.lineTo(0, 1000);
        path.lineTo(0, 0);

        // only needed when you don't close the path.
        path.close();

        mPolygonWrapView.setPath(path);
        mPolygonWrapView.setFontSize(30);

        mPolygonWrapView.setBackgroundColor(0xFFFFFFFF);

        mPolygonWrapView.invalidate();
    }
}

我在这里进行了测试,似乎可以很好地工作,至少足以让您入门。

您可能想添加一些改进,例如确保整个行的高度都在多边形内,而不仅仅是行的中心Y坐标。

此外,您可能希望支持路径列表,而不仅仅是一个路径 —— 这样您就可以控制文本在路径段之间的分布方式,而不像我在这里所做的那样进行一个x/y盒子填充。

您还可以改进算法,通过调整为空格分配的像素数量,使所有文本行正确地夹紧到行尾。例如,如果一行没有足够的空间来容纳另一个单词,但是没有那个单词,该行会明显在多边形的末端之前结束,您可以增加每个单词之间的间距宽度,使该行的最后一个单词恰好在多边形边缘结束。实现这个需要改变我的算法,在绘制之前预处理行,但不应该太难。

如果您有任何进一步的问题,请告诉我。

编辑:我已编辑使用示例,以显示如何使用bezier曲线实现此路径。这里提供了有关如何使用路径创建bezier曲线的参考资料,以获取更多详细信息。


谢谢您抽出时间回答这个问题,我会尝试并让您知道结果。 - Shruti
我仍然不清楚如何将贝塞尔曲线添加到路径中,请也给我一些建议。 - Shruti
我已经编辑了答案,包括使用贝塞尔曲线构建路径。请参见MainActivity.java(用法示例)。 - Gil Moshayof
并没有帮助我太多,我仍然无法得到预期的输出。 - Shruti
好的,你的问题要求一个视图来围绕可能包含贝塞尔曲线的路径排列文本,而这正是这个视图所做的(已经测试和确认)。我不明白为什么这不能算作“没有帮助”,因为我完全实现了整个视图来解决你的问题。也许你可以更具体一些? - Gil Moshayof

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接