Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Wrap text outside bezier curve in android

I want to wrap text in the shape of bezier curve outside the curve in android.

What I have tried :

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

What I am trying to achieve :

But this code draws text on curve..I do not want to write text on curve..I want to wrap text according to the curve and write it on next line.

To understand it clearly please refer baconforme.com..I want to create this jquery like behaviour in android without using webbrowser.

and saw this link On Android how do I wrapping text inside in a bezier path

Question :

  1. Is it possible to achieve this ?
  2. If yes,then please guide me.
like image 697
Shruti Avatar asked Apr 16 '15 09:04

Shruti


1 Answers

I've implemented a basic view which performs what you're trying to do. The idea here is to create a bitmap out of the path you requested. Each pixel outside the path will have a 0 value, and each pixel inside the path will have some other value.

With this, you can know when a point is within the polygon or not. Now we need to determine where we draw the text.

I will generate a list of rectangles by traversing the generated bitmap. Each rectangle will define rows beginning and ending inside the polygon.

With each rectangle, I begin to populate text until the rectangle can no longer accommodate more text, in which case I move to the next rectangle. Once there are no more rectangles, or I'm out of text, I stop drawing.

In this implementation, I added some customisation such as font size, text colour and wrapping mode.

Here it is:

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: (usage example)

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

I've tested it here and it seems to work well, at least enough to get you started.

You might want to add some improvements, such as making sure the entire row's height is within the polygon, not just the centerY of the row.

Also, you might want to support a list of paths, rather than just 1 path - that way you could control how text is distributed between path segments, rather than doing an x/y box fill like I'm doing here.

You might also want to improve the algorithm to make all the text rows clamp to the end of the row correctly by adjusting the amount of pixels you allot for the spaces. For example, if a row doesn't have enough space to fit an additional word, but without that word the row ends noticeably before the end of the polygon, you could increase the space width between each word to make the last word of the row end exactly where the polygon edge is. Implementing this would require changing my algorithm to preprocess the row before drawing it, but shouldn't be too difficult.

Let me know if you have any further questions.

Edit: I've edited the usage example to show how to implement this path with a bezier curve. Here is a reference to how to create a bezier curve with a path for more details.

like image 82
Gil Moshayof Avatar answered Sep 18 '22 14:09

Gil Moshayof