0

I've got an SQLite database with a units table. The units table is set up with only two columns:

create table units (_id INTEGER PRIMARY KEY, desc TEXT)

Example data for a row in this table is:

  • _id: 4
  • desc: "Helix #5 [2231]"

The "[2231]" substring is important, and I'd like to change its color to a medium gray color. Id also prefer to do this to the data in the desc column, as opposed to manipulating it with java.

So, I query for the data:

/**
 * Get all unit records for display in spinner
 */
public Cursor getAllUnitRecords(){
    String sql = "select * from units order by `desc`";
    return db.rawQuery(sql, null);
}

My spinner looks like this:

<Spinner
    android:id="@+id/UnitSpinner"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:spinnerMode="dropdown" />

And I get the data to the spinner like this:

// Prepare unit dropdown
Cursor units = db.getAllUnitRecords();
MatrixCursor unitsMatrixCursor = new MatrixCursor(new String[] { "_id", "desc" });
unitsMatrixCursor.addRow(new Object[] { 0, "" });
MergeCursor unitsMergeCursor = new MergeCursor(new Cursor[] { unitsMatrixCursor, units });
String[] unitsFrom = new String[]{"desc"};
int[] unitsTo = new int[]{android.R.id.text1};
Spinner unitSpinner = (Spinner) findViewById(R.id.UnitSpinner);
SimpleCursorAdapter unitAdapter = new SimpleCursorAdapter(this, android.R.layout.simple_spinner_dropdown_item, unitsMergeCursor, unitsFrom, unitsTo, 0);
unitAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
unitSpinner.setAdapter(unitAdapter);

Since I'd like to color the "[2231]" substring a medium gray color, I thought I might be able to change the value of desc in the database, so that it looks like this:

"Helix #5 <font color='#6e737e'>[2231]</font>"

I did that only because I was searching the internet, and it seemed like it might work. Well, that doesn't work, as the tags are just output, instead of changing the color. What is wrong, and how can I fix it? I guess I'm open to a different solution if necessary, but this Android stuff is hard for me, as I don't work on it very often, so I was trying to go for the easiest solution.

UPDATE #1 ----------------------

So @MartinMarconcini was kind enough to point me in the right direction, and I copy and pasted his colorSpan method into my activity class to test it out. I then looked all around Stack Overflow for any clues as to how to modify the text of my spinner, and then how to modify the text that's in a SimpleCursorAdapter.

I found these questions with answers:

That gave me some ideas, so I tried to work with that:

// Prepare unit dropdown
Cursor units = db.getAllUnitRecords();
MatrixCursor unitsMatrixCursor = new MatrixCursor(new String[] { "_id", "desc" });
unitsMatrixCursor.addRow(new Object[] { 0, "" });
MergeCursor unitsMergeCursor = new MergeCursor(new Cursor[] { unitsMatrixCursor, units });
String[] unitsFrom = new String[]{"desc"};
int[] unitsTo = new int[]{android.R.id.text1};
Spinner unitSpinner = (Spinner) findViewById(R.id.UnitSpinner);
SimpleCursorAdapter unitAdapter = new SimpleCursorAdapter(this, android.R.layout.simple_spinner_dropdown_item, unitsMergeCursor, unitsFrom, unitsTo, 0);
unitAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);

/* NEW CODE STARTS HERE */
unitAdapter.setViewBinder(new SimpleCursorAdapter.ViewBinder() {

    public boolean setViewValue(View aView, Cursor aCursor, int aColumnIndex) {

        if (aColumnIndex == 1) {
            String desc = aCursor.getString(aColumnIndex);
            TextView textView = (TextView) aView;
            final Spannable colorized = colorSpan(desc);
            textView.setText(TextUtils.isEmpty(colorized) ? desc + "a" : colorized + "b");
            return true;
        }

        return false;
    }
});
/* NEW CODE ENDS HERE */

unitSpinner.setAdapter(unitAdapter);

Notice I added the letter "a" if there was no text, and "b" if there was text. Sure enough, the "a" and "b" were added to my spinner items, but there was no color change! So, I am trying ... but could still use some help. Here is an image of what I'm seeing:

Screenshot of Android Phone

Brian Gottier
  • 4,522
  • 3
  • 21
  • 37
  • Possible duplicate of [Set color of TextView span in Android](https://stackoverflow.com/questions/3282940/set-color-of-textview-span-in-android) – Martin Marconcini Aug 25 '17 at 19:45
  • This question has nothing to do with TextViews. – Brian Gottier Aug 25 '17 at 19:49
  • The textView is _irrelevant_, the question is about “coloring a string”. It’s done with Spannables. The question will guide you to that. – Martin Marconcini Aug 25 '17 at 19:51
  • I was hoping for a pre-styled solution. If it's not possible, then just say so. – Brian Gottier Aug 25 '17 at 19:52
  • Unfortunately, there is none that I am aware of. It can be abstracted in a custom component and what not, but the backend of the operation is a `Spannable`. Since you have some form of `token` you can maybe use the brackets as start/end detection or something like that. But without more information, it’s impossible to tell what the best particular approach would be. I assume the idea of the color is so it’s presented as such (colored). If that’s the case, the DB should have no idea about color, that’s a presentation problem and belong in another layer in your app. – Martin Marconcini Aug 25 '17 at 19:55
  • Bracketed number is always on the end of the string, but may vary in length from [1] to [999999]. Looking at your link, I see how Spannable is being used for that, but have no idea where this would be done in my case. I wouldn't be asking if I had a clue. – Brian Gottier Aug 25 '17 at 19:58
  • ok, let me put an answer w/more info. – Martin Marconcini Aug 25 '17 at 20:07
  • See answer. I know you want to use a spinner, but I don’t have time atm to test what options the spinner exposes. It *should* work like any other android Text, but Android works in mysterious and broken ways sometimes, so if the Spinner is not respecting the spannables… you may have to roll your own solution. Remember a Spannable is “Editable” in the end (look at the source code, they are all interfaces that extend from CharSequence, etc. So work with Spannables instead of Strings. – Martin Marconcini Aug 25 '17 at 20:28
  • I'm going to give it a shot. Might take hours and hours, but I'll for sure report back. Thanks for your patience and the help. This is way different that what I'm used to working on. – Brian Gottier Aug 25 '17 at 20:29

2 Answers2

1

As mentioned in the comments, the presentation shouldn’t be tied to the logic. This is a presentation problem. You want to display a text and you want part of that text to be colored.

So, anywhere in your app where you need to display/present this text to the user, say…

someTextViewOrOtherWidget.setText(yourString);

…you’ll then have to call a method that does the coloring for you.

Example…

I’d move this code into a separate method/place for reuse and make it more re-usable by not hardcoding the [] and such,but this is how a simple example would look:

private Spannable colorSpan(final String text) {
    if (TextUtils.isEmpty(text)) {
        // can't colorize an empty text
        return null;
    }

    // Determine where the [] are.
    int start = text.indexOf("[");
    int end = text.indexOf("]");

    if (start < 0 || end < 0 || end < start) {
        // can't find the brackets, can't determine where to colorize.
        return null;
    }

    Spannable spannable = new SpannableString(text);

    spannable.setSpan(
            new ForegroundColorSpan(Color.BLUE)
            , start
            , end
            , Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
    );

    return spannable;
}

And you’d use it like…

    String text = "Hello [123123] how are you?";
    final Spannable colorized = colorSpan(text);

    textView.setText(TextUtils.isEmpty(colorized) ? text : colorized);

I hope this gives you a better idea how to get started.

Martin Marconcini
  • 26,875
  • 19
  • 106
  • 144
  • I've done what I can do, and while your answer is helpful, I'm still not able to colorize that text. I updated my question with what I did, and a screenshot of the result. Surely seems like it should be working, but it's not. – Brian Gottier Aug 25 '17 at 22:31
  • I see… just to (because I haven’t) make sure, have you tried that a TextView does look colorized? Just add a simple text view and see if it looks with color. I want to rule out that this is not a problem with the spinner in particular. (which I think it is, spinners are… strange) – Martin Marconcini Aug 25 '17 at 22:43
  • I have the feeling that the spinner is taking the style from the theme and therefore ignoring any spannable. I don’t see (by looking at the Spinner’s source code) any TextView exposed. Maybe you’ll have to provide your own custom adapter for the spinner, and provide a “custom” view that you inflate and contains a real TextView you then proceed to cast to `(TextView)` so you can do `setText(spannable)` or something. – Martin Marconcini Aug 25 '17 at 22:56
  • OK , so I tried to colorize a TextView, and it does apply the color. Since you know what you're doing and I don't, you're most likely right about the solution, but your solution sounds way above my Android Studio user skill level. I can start looking into it, but if you've got any bones to throw my way, they'd be much appreciated. – Brian Gottier Aug 26 '17 at 00:12
  • Right. So the Spinner sucks as I suspected. Unfortunately I can’t provide a full solution atm, and this requires a deeper look. If I had to give a newcomer hints on what to look for, I’d say these are the things you need to learn: 1. How to use a Custom adapter in your spinner (instead of just relying on SimpleCursorAdapter doing all for you). You can `extend` from it, but then your subclass would allow you to customize certain things. 2. Learn how to use a custom `layout` for your spinner (used by the adapter) in which the “layout” for each “row” is something you create and provide… (cont.) – Martin Marconcini Aug 26 '17 at 00:47
  • … this way, you can have a simple Layout that contains a TextView which will most likely do what you want (display the color!). The CursorAdapter (which I haven’t used in like 3 years) “binds” views for each line of data, you need to provide your own “binder” to put your own “view”. The adapter doesn’t know much about views by itself. – Martin Marconcini Aug 26 '17 at 00:55
  • 1
    Thanks Martin. I got it all figured out (see below). Couldn't have done it without you, so I'm giving you the credit for the answer. – Brian Gottier Aug 26 '17 at 08:04
  • Glad to hear you have gotten a working solution, Brian! Good luck with your project! – Martin Marconcini Aug 26 '17 at 21:29
0

So, with a lot of help from @MartinMarconcini, I finally achieved what needed to be done, and so I wanted to leave "the answer" here, in case anyone else wants to see what needed to be done. I ended up making a custom cursor adapter, and although I'm still dumbfounded by the complexity of Android Studio, it works!

First, in the activity, the way SimpleCursorAdapter was being used ended up getting changed to the custom cursor adapter (which extends SimpleCursorAdapter).

These lines:

Spinner unitSpinner = (Spinner) findViewById(R.id.UnitSpinner);
SimpleCursorAdapter unitAdapter = new SimpleCursorAdapter(this, android.R.layout.simple_spinner_dropdown_item, unitsMergeCursor, unitsFrom, unitsTo, 0);
unitAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
unitSpinner.setAdapter(unitAdapter);

Were replaced with these lines:

Spinner customUnitSpinner = (Spinner) findViewById(R.id.UnitSpinner);
UnitSpinnerCursorAdapter customUnitAdapter = new UnitSpinnerCursorAdapter(this, R.layout.unit_spinner_entry, unitsMergeCursor, unitsFrom, unitsTo, 0);
customUnitSpinner.setAdapter(customUnitAdapter);

I put the custom cursor adapter in its own file, and I put Martin's colorSpan method in there too (for now):

package android.skunkbad.xxx;

import android.content.Context;
import android.database.Cursor;
import android.graphics.Color;
import android.support.v4.widget.SimpleCursorAdapter;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

public class UnitSpinnerCursorAdapter extends SimpleCursorAdapter {

    private Context context;
    private int layout;

    public UnitSpinnerCursorAdapter(Context context, int layout, Cursor c, String[] from, int[] to, int flags) {

        super(context, layout, c, from, to, flags);
        this.context = context;
        this.layout = layout;

    }

    /**
     * newView knows how to return a new spinner option that doesn't contain data yet
     */
    @Override
    public View newView(Context context, Cursor cursor, ViewGroup parent) {
        super.newView(context, cursor, parent);

        Cursor c = getCursor();

        final LayoutInflater inflater = LayoutInflater.from(context);
        View v = inflater.inflate(layout, parent, false);

        int descCol = c.getColumnIndex("desc");
        String desc = c.getString(descCol);
        final Spannable colorized = colorSpan(desc);

        TextView unit_spinner_entry = (TextView) v.findViewById(R.id.custom_spinner_entry_desc);

        if (unit_spinner_entry != null) {
            unit_spinner_entry.setText(TextUtils.isEmpty(colorized) ? desc : colorized);
        }

        return v;
    }

    /**
     * bindView knows how to take an existing layout and update it with the data pointed to by the cursor
     */
    @Override
    public void bindView(View view, Context context, Cursor cursor) {
        super.bindView(view, context, cursor);

        int descCol = cursor.getColumnIndex("desc");
        String desc = cursor.getString(descCol);
        final Spannable colorized = colorSpan(desc);

        TextView unit_spinner_entry = (TextView) view.findViewById(R.id.custom_spinner_entry_desc);

        if (unit_spinner_entry != null) {
            unit_spinner_entry.setText(TextUtils.isEmpty(colorized) ? desc : colorized);
        }

    }

    private Spannable colorSpan(final String text) {
        if (TextUtils.isEmpty(text)) {
            // can't colorize an empty text
            return null;
        }

        // Determine where the [] are.
        int start = text.indexOf("[");
        int end = text.indexOf("]");

        if (start < 0 || end < 0 || end < start) {
            // can't find the brackets, can't determine where to colorize.
            return null;
        }

        end++; /* Why do we even need this ? */

        Spannable spannable = new SpannableString(text);

        spannable.setSpan(
                new ForegroundColorSpan(Color.rgb(100,100,100))
                , start
                , end
                , Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
        );

        return spannable;
    }

}

Finally, I had to make a layout file for each entry in the spinner:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="50dp">


    <TextView
        android:id="@+id/custom_spinner_entry_desc"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="16dp"
        android:textColor="#FFFFFF"
        android:layout_marginLeft="10dp" />

</LinearLayout>

Thanks Martin! It might seem like nothing to you, but it was hard for me, and I couldn't have done it without your help.

One note: I had to put end++; in your colorSpan method, because for some reason it wasn't coloring the closing bracket.

Brian Gottier
  • 4,522
  • 3
  • 21
  • 37
  • Good job Brian! Nice implementation. A couple of comments. `end++; /* Why do we even need this ? */`. -> You need that because my code had a *bug* and you fixed it ;) (`indexOf` is returning the index where the first occurrence of a character appears. In the string 012[4]6 the index for the closing bracket will be 5, imagine the values are comma separated: 0,1,2,[,4,],6 now count the commas you need to reach your character. ;) So your end+1 is fine for the span, otherwise it’s not included. – Martin Marconcini Aug 26 '17 at 21:35
  • An additional comment is: try not to use variable names like PHP (`where_everything_is_named_like_this`), it won’t obviously affect your code and it’s irrelevant, but… when you share people with the Android/Java community, a small degree of consistency is preferred (if anything, because it makes things easier to read). So, get used to press cmd+option-L (to format your files) and to use `camelCaseForVariables` ;) Makes it a lot more approachable. You also use final randomly, and even tho it’s usually not required (and doesn’t provide a perf. improvement anymore), it’s better to be consistent… – Martin Marconcini Aug 26 '17 at 21:37
  • so you either use it in all your local variables/params (where appropriate) or you skip it altogether. (final means that a REFERENCE cannot be changed). Additionally, put the `end++` directly in the `setSpan(` method, you don’t need to increment it out there. Last but not least, I suggest you use `layout_marginStart` (not left) and `…end` instead of Right. It’s the internationalization-friendly version of the same. (in case you ever need RTL languages). ;) Anyway. Good job and keep up the good work. And don’t worry, I haven’t used a SimpleCursorAdapter in ages, you refreshed my mem, big thing! – Martin Marconcini Aug 26 '17 at 21:40