9

I've been trying this since morning, yet I can't get it to work.

What I'm trying to do is create a somewhat like long shadow for the TextView, which is similar to the following:

http://www.iceflowstudios.com/v3/wp-content/uploads/2013/07/long_shadow_banner.jpg http://web3canvas.com/wp-content/uploads/2013/07/lsd-ps-action-720x400.png

My solution so far was to create a lot of TextViews and cascade them under each other, but there are a lot of performance issues if I go with the current way.

Another solution is the usage of a custom font that has that similar allure, yet I cannot find any that matches the font I am currently using.

So I was wondering, is it possible to use: (I have to mention, the textviews are created dynamically)

TV.setShadowLayer(1f, 5f, 5f, Color.GREY);

To create several of them in a line (as a cascading layer), making the shadow seem smooth? Or do you guys suggest any other solutions?

Thanks in advance.

Johnaudi
  • 257
  • 1
  • 23

3 Answers3

12

Try to play with raster images:

  1. Detect bounds of text using Paint.getTextBounds() method
  2. Create transparent Bitmap with such metrics (W + H) x H (you may use Bitmap.Config.ALPHA_8 to optimize memory usage)
  3. Draw text on this Bitmap at 0x0 position
  4. Copy first row of Bitmap into new one with original width, but with height of 1px
  5. Iterate over the Y-axis of Bitmap (from top to bottom) and draw single-line Bitmap with the corresponding offset by X-axis (you will overdraw some transparent pixels)
  6. Now you have the top-part of your shadow
  7. Draw the bottom part using same technique, but choosing last row of this Bitmap

This algorithm may be optimized if you detect, that all pixels in last row have the same color (full shadow).

UPDATE 1

I achieved such result using this quick solution:

enter image description here

MainActivity.java

import android.app.Activity;
import android.os.Bundle;

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle state) {
        super.onCreate(state);

        LongShadowTextView longShadow = new LongShadowTextView(this);
        longShadow.setText("Hello World");
        setContentView(longShadow);
    }
}

LongShadowTextView.java

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.view.View;

public class LongShadowTextView extends View {
    private Bitmap mBitmap;
    private String mText;

    public LongShadowTextView(Context context) {
        super(context);
    }

    public void setText(String text) {
        Paint paint = new Paint();
        // TODO provide setters for these values
        paint.setColor(Color.BLACK);
        paint.setTextSize(142);

        Rect rect = new Rect();
        paint.getTextBounds(text, 0, text.length(), rect);

        Bitmap bitmap = Bitmap.createBitmap(rect.width() + rect.height(), rect.height(), Bitmap.Config.ALPHA_8);
        Canvas canvas = new Canvas(bitmap);

        canvas.drawText(text, 0, rect.height(), paint);

        Rect src = new Rect();
        RectF dst = new RectF();

        int w = bitmap.getWidth();
        int h = bitmap.getHeight();

        src.left = 0;
        src.right = w;

        for (int i = 0; i < h; ++i) {
            src.top = i;
            src.bottom = i + 1;

            dst.left = 1;
            dst.top = i + 1;
            dst.right = 1 + w;
            dst.bottom = i + 2;

            canvas.drawBitmap(bitmap, src, dst, null);
        }

        mText = text;
        mBitmap = bitmap;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawBitmap(mBitmap, 0, 0, null);
    }
}

UPDATE 2

Here is final result which I achieved. Clone this demo from github.

enter image description here

Oleksii K.
  • 5,359
  • 6
  • 44
  • 72
  • so what we're doing here is drawing the shadow in the bitmap without having to let it "view" every textview at a time, and then showing just the bitmap, right? – Johnaudi Oct 16 '14 at 15:22
  • exactly! check my updates, this is very hot solution. now all you need - just to copy last raw from `mBitmap` inside `onDraw()` method and draw original text above shadow. – Oleksii K. Oct 16 '14 at 15:28
  • I mean last row (instead *raw*) – Oleksii K. Oct 16 '14 at 16:40
  • I haven't tested it yet, but I'm going to give you the bounty for putting so much work and effort, thank you! – Johnaudi Oct 16 '14 at 17:23
  • 2
    hey there, I resolved your task completely! check this demo: https://github.com/shamanland/longshadowtextview – Oleksii K. Oct 16 '14 at 21:36
  • it's working great on my app, tried it 3 hours ago... Thanks a bunch pal! - Also, you don't have to draw another bitmap, making it Bitmap.createBitmap(rect.width() + rect.height() + 50, rect.height() + 50, Bitmap.Config.ALPHA_8); adds in for distance, which I found it way easier than individually draw a bitmap over its layer. – Johnaudi Oct 16 '14 at 21:37
  • Thanks for sharing this code. I already upvoted this answer, it's working quite well. The only issue I spotted is that the shadow isn't fitting, when tall letters (like 'g', 'j') are used. Do you have any idea how to fix this? It seems that the TextBounds arent correct when these letters are used. – Martin Pfeffer Feb 07 '16 at 00:08
  • Perhaps we need to handle correctly ascent and descent values of font. Read more here: https://en.wikipedia.org/wiki/Ascender_(typography) – Oleksii K. Feb 07 '16 at 17:16
  • FATAL EXCEPTION: main java.lang.IllegalArgumentException: width and height must be > 0 at android.graphics.Bitmap.createBitmap(Bitmap.java:638 – Ninja Coding Jun 02 '16 at 18:35
  • this is'nt working for a TextView with a custom Typeface – Ninja Coding Jun 02 '16 at 18:48
0

I'm afraid your suggested approach of using setShadowLayer() wont work as this approach effectively draws a second TextPaint with blurring.

Superimposing several TextPaints on top of each other will essentially mean you need to offset it by 1px for each step, which is very graphically intensive and will have a very poor performance.

This is an excellent question and a real challenge!

The only solution that comes to mind is to handle each glyph independently, inspecting all path elements and extending a shadow between the furthest bottom-left and top-right point. This seems very complicated, and I don't know if there's any mechanics in the SDK that facilitates an approach like that.

Suggested reading:

Community
  • 1
  • 1
Paul Lammertsma
  • 37,593
  • 16
  • 136
  • 187
  • Thank you for this answer, but can you please elaborate on how would that make me able to draw off the shadow? Would I have to change the glyph's path to a somewhat custom direction then draw it on a bitmap? I'm kind-of having trouble understanding this. – Johnaudi Oct 12 '14 at 15:12
  • I would tackle this using a custom view, drawing each glyph as a path, and inspecting the Path of each character to draw the extended shadow beyond it using a different Paint. It seems like a rather daunting task, however. – Paul Lammertsma Oct 12 '14 at 15:21
  • Any idea if your way would require a lot of performance to execute? I'm expecting to have this done on over 5+ TextViews. Also, it would be really helpful if you could provide me with an example since I haven't fully understood how the mechanics would work. Thanks again! – Johnaudi Oct 12 '14 at 15:24
  • Having delved into it a bit deeper, it seems that there's no Java implementation of drawing glyphs in Android. The code used in TextView calls through to the Layout used, in turn calling into TextLine, which uses [Canvas.drawTextRun()](http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.4.4_r1/android/graphics/Canvas.java#Canvas.drawTextRun%28java.lang.CharSequence%2Cint%2Cint%2Cint%2Cint%2Cfloat%2Cfloat%2Cint%2Candroid.graphics.Paint%29) to draw text through native code. – Paul Lammertsma Oct 12 '14 at 16:20
  • I'm afraid there's no existing Java implementation in the SDK of obtaining a path from a glyph, so taking this approach is going to be a massive amount of work. You'd have to interpret the TTF yourself, creating a Path for each glyph, and drawing it onto a canvas using `drawPath()`. For extending the shadow, you'd need to navigate the path, and determine the boundaries of each closed path to figure out how to extend it inside the view. – Paul Lammertsma Oct 12 '14 at 16:22
  • The thing with the font, is that I need it to cut down at a certain level, and what I need is to be able to dynamically select how far away the tail would be, not just some constant. – Johnaudi Oct 12 '14 at 17:07
0

Small comment if someone would try to run setText() method. it is not working now. You should call invalidate(); in setText(); method

 public void setText(String value) {
    boolean changed = 
       mText == null && value != null || mText != null && !mText.equals(value);

    mText = value;

    if (changed) {
        refresh();
    }
    invalidate();
}
rurik
  • 11
  • 2