Обтекание текста вне кривой Безье в андроиде

Я хочу обернуть текст в виде кривой Безье вне кривой в android.

Что я пробовал:

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

То, что я пытаюсь достичь:

Но этот код рисует текст на кривой. Я не хочу писать текст на кривой. Я хочу обернуть текст в соответствии с кривой и записать его на следующей строке.

Чтобы понять это, пожалуйста, обратитесь baconforme.com. Я хочу создать этот jquery как поведение в android без использования веб-браузера.

и увидел эту ссылку На Android, как я могу обернуть текст внутри в пути безье

Вопрос:

  • Можно ли достичь этого?
  • Если да, то пожалуйста, направляйте меня.

Ответы

Ответ 1

Я реализовал базовый вид, который выполняет то, что вы пытаетесь сделать. Идея здесь состоит в том, чтобы создать растровое изображение из запрошенного вами пути. Каждый пиксель за пределами пути будет иметь значение 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, как я здесь делаю.

Вы также можете улучшить алгоритм, чтобы сделать все строки строк строк до конца строки правильно, отрегулировав количество пикселей, которые вы выделяете для пробелов. Например, если в строке недостаточно места для размещения дополнительного слова, но без этого слова строка заканчивается заметно до конца многоугольника, вы можете увеличить ширину пространства между каждым словом, чтобы сделать последнее слово строки конец, где находится край многоугольника. Реализация этого потребует изменения моего алгоритма для предварительной обработки строки перед ее рисованием, но не должна быть слишком сложной.

Сообщите мне, если у вас есть дополнительные вопросы.

Изменить:. Я редактировал пример использования, чтобы показать, как реализовать этот путь с помощью кривой безье. Здесь приведена ссылка на то, как создать кривую Безье с дорожкой для более подробной информации.