How can I use Androids Talkback with clickableSpan? - android

So I have a text of variable length and until now I have filled it with a SpannableString to highlight specific "hard" words you can then click on to get an explanantion in a dialog. But because I have to design my application for accessibility I neeed androids Talkback feature to read out these words (plus the text surrounding it, but I've got that for now) as well as being able to click them. So far I haven't found a way even to click on the ClickableSpan without disabling Talkback.
I found something about ClickableSpan not being able to handle Acessibility but URLSpan is? If that is the case, can I open a dialog with custom text with a URLSpan? Or does it have something to do with me calling text.setMovementMethod(LinkMovementMethod.getInstance()); ?
Thanks in advance, it's been really hard to find anything on Accessibility, not many programmers seem to care much.

Talkback users will use local context menu to access links within text: https://support.google.com/accessibility/android/answer/6007066?hl=en

Maybe this can be the easiest solution..it worked for me.
textView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
ClassroomLog.log(TAG, "Textview Click listener ");
if (textView.getSelectionStart() == -1 && textView.getSelectionEnd() == -1) {
// Perform your action here
}
}
});

So I finally did find a way to use Talkback with SpannableStrings. Well, not really, but it's a workaround. I removed the ClickableSpan from the TextView but stored the start and end positions and put the textView into another Layout.
I then iterated through the stored positions and added empty Views right on top of the text with a fitting ContentDescription and the onClick property I needed in it's onClickListener. To get the Views position and size I used this code:
//clickableSpans is an int[] Array with the start and end character positions of the previous clickableSpans stored after each other
for (int i = 0; i < clickableSpans.length; i +=2) {
int width= (int) (layout.getPrimaryHorizontal(clickableSpans[i+1]) - (int) layout.getPrimaryHorizontal(clickableSpans[i]));
int height= layout.getLineBottom(layout.getLineForOffset(clickableSpans[i])) - layout.getLineTop(layout.getLineForOffset(clickableSpans[i]));
int xOffset = (int) layout.getPrimaryHorizontal(clickableSpans[i]);
int yOffset = layout.getLineTop(layout.getLineForOffset(clickableSpans[i]));
//Now position an empty View according to those values
}

Related

Android web link in textView inside ListView

I want to create a link in text view.
My link look like so:
The text to the link I get from array.xml
<item>Icons made by Freepik</item>
I already read set movement method
textView.setMovementMethod(LinkMovementMethod.getInstance());
This has no impact
and
android:autoLink="web"
This works if the text is http://www.freepik.com, but not if I want to have a custom text as link.
viewHolder.textView.setClickable(true);
viewHolder.textView.setText(text);
viewHolder.textView.setMovementMethod(LinkMovementMethod.getInstance());
This is a code which I am using to fill textView
I want in the end text looking like so:
Icons made by Freepik
I think you can't accomplish what you want in this way.
I think the simplest solution is to separate your links in differents list items. Keep in mind that you could use different TextView with different heights for example
Alternatively you could pass to a custom view approach. If you create a custom view (for example MultiLinkView), then you could add this view to the ListView.
I suggest this solution because this approach allow you to add a powerful logic to the view item.
I can't give you the complete code because it should be too long, but I can put you in the right way.
A custom view is a real Java class that extends some Android view class. So when you instantiate a CustomView you can pass to its constructor all the params you want (references, links, arrays and so on).
Start here
My idea is to find a way to pass all the parameters you need to your custom view and then find a way to represent your data, mapping them to your links.
I think you should abandon html solution in favor to ClickableSpan.
This is a piece of code that I used in a project to make clickable a single part of my string:
String text = "Hello <b>click me!</b> to go to internet!";
// create Spanned
Spanned spanned = Html.fromHtml(text);
// create SpannableString
SpannableString spanString = new SpannableString(spanned);
// set clickable part
ClickableSpan clickablePart = new ClickableSpan() {
#Override
public void onClick(View textView) {
if (connectionDetector.isConnectedToInternet()) {
// open browser or webview fragment
}
}
#Override
public void updateDrawState(TextPaint ds) {
super.updateDrawState(ds);
ds.setUnderlineText(false);
ds.setColor(Color.WHITE);
}
};
int startClickMe = spanString.toString().indexOf(text);
spanString.setSpan(clickablePart, startClickMe, text.length() + startClickMe, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Obviously in the onClick you should find a way to get the right link, but, as I said before, in a custom view you can put as many variables as you want. I'm sure that you can find a solution.
Let me know if it helps

Set text and show TextView conditionally in a RecyclerView

I'm displaying a list alphabetically in a RecyclerView,
each letter group should have a letter displayed to the left side, once, at the top of each group.
The divider line is rendered in an ItemDecorator.
I'm trying to solve this in onBindViewHolder.
The initial layout works fine. As I scroll from the top to bottom everthing is as expected.
But when I scroll back up the the initial/capital letter goes missing or it gets reordered.
Scrolling down is showing the initial letter conditionally as expected:
After scrolling back up k is missing in this example, :
public void onBindViewHolder(WordItemViewHolder wordItemViewHolder, final int position) {
final WordModel wordModel = wordModels.get(position);
wordItemViewHolder.textView.setText(wordModel.getWord());
String word = wordModel.getWord();
String currentFirstLetter = word.substring(0,1);
if(maxListRendered <= position){
if(!previousLetter.contentEquals(currentFirstLetter) || position == 0){
wordItemViewHolder.initialView.setText(currentFirstLetter.toUpperCase());
wordItemViewHolder.initialView.setVisibility(View.VISIBLE);
previousLetter = currentFirstLetter;
}else{
wordItemViewHolder.initialView.setVisibility(View.INVISIBLE);
}
}
maxListRendered++;//initialised as 0 in attempt to track calls to onBindViewHolder
}
Any help appreciated, thank you.
Get rid of maxListRendered and previousLetter to begin with - that's dangerous to do and will cause issues.
Instead check the index above the current. Also instead of showing and hiding elements, I'd recommend having different view types by overriding getItemViewType - that makes them being recycled separately so they can have different views.

Recyclerview: expanding a textview

I have a textView inside a recyclerview currently that looks like this:
The full text says: "This is the place where I would put the information about the food item. I want to truncate the line to only show 3 lines and then when I press on the caret, it will expand to the maximum amount of lines."
If I click on the caret (the little down arrow at the top), I want it to expand so that I can see all the lines.
In my recyclerview adapter, I have the following code:
if (holder instanceof InfoViewHolder) {
((InfoViewHolder) holder).more.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (!expandedInfo) {
((InfoViewHolder) holder).info.setMaxLines(Integer.MAX_VALUE);
expandedInfo = true;
}
((InfoViewHolder) holder).info.setMaxLines(3);
notifyItemChanged(position);
}
});
}
I'm just manipulating the maxLines in the textView to try and expand the space it will take in the textView so that the view will adjust itself when I notifyItemChanged, however, this does not work and my information textview does not expand.
Can anyone tell me how I can get this to work properly?
Using this library was probably the easiest and the quickest way to solve this puzzle:
https://github.com/Manabu-GT/ExpandableTextView
Although, I would have like to have done it the old fashion way instead of importing a new library so if anyone else has any ideas on how to do this without a library, feel free to post your suggestions below.
The reason it didn't work because ((InfoViewHolder) holder).info.setMaxLines(3); is called no matter what.
So, inside the OnClickListener, it should be:
int maxLines = expandedInfo ? Integer.MAX_VALUE : 3;
((InfoViewHolder) holder).info.setMaxLines(maxLines);
notifyItemChanged(position);
Also, about the ExpandableTextView library, currently it doesn't work in RecyclerView. See my comments on the issue.

Why setting text from onMeasure does not affect TextView?

There is a TextView of a certain size and whenever text set to it is too long I'd like to re-set it to some preset value.
To accomplish this I am overriding onMeasure and calling setText. However this does not affect the TextView contents.
What am I missing?
EDIT: if you think that it should be done in a completely different way - please feel free to suggest
onMeasure() is usually called during layout phase. Besides as far as I know onMeasure() is where YOU have to measure your view. It receives 2 values which are the size of the parent of your view which may be constant.
Why don't you just check the length of the text you're setting and if it's too long just replace it with your default one?
From the View documentation page.
Measure the view and its content to determine the measured width and
the measured height. This method is invoked by measure(int, int)
and should be overriden by subclasses to provide accurate and
efficient measurement of their contents.
If you want the textview to have limited width or height call setMaxWidth() setMaxHeight() setMaxLines() or check it manualy and change is the way you like from a custom method
In your onMeasure(int,int)
CALL super(int,int)
Use getMeasuredWidth() and getMeasuredHeight
something like this:
void onMeasure(int a, int b){
super(a,b);
if(getMeasuredWidth()>bla)
setText("default");
}
This is what I do to reduce the text font in case the text content is large enough to goto the next line. The problem is pretty much the same the only difference in your case is that you want to replace it with some other default text.
ViewTreeObserver vto = ((TextView)findViewById(R.id.myTextView)).getViewTreeObserver();
vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
if (null != ((TextView)findViewById(R.id.myTextView))) {
if (1 < ((TextView)findViewById(R.id.myTextView)).getLineCount()) {
((TextView)findViewById(R.id.myTextView)).setTextSize(TypedValue.COMPLEX_UNIT_PX,
((TextView)findViewById(R.id.myTextView)).getTextSize() - 2);
}
}
}
});
This would take care of your length dynamically. Now the text size or length cannot be fixed as based on different form factors it would change. For instance a 20 character text in 3.2 inch screen would be decidedly large however it would be too small for 4.1 inch screen. So I would suggest you to use line count as the reference and if the text wraps then you can either reduce the font like I do or replace it by something else.
You could register a custom TextWatcher with addTextChangedListener (TextWatcher watcher).
Inside the TextWatcher you would override the afterTextChanged(Editable s) function, mesure the lenght of the text, and if too long, set it back to your default text.
EDIT: I'm not too sure about this, but this is why I think your solution with onMeasure is not working: onMeasure is called on the initial layout phase, or when the UI needs to be resized, so once your UI is set, if you change the text of the UI afterwards, and the text becomes too long, this doesn't affect the size of the TextView, and onMeasure is not called again...
AnyWay better to just look out for text changes, and do what you want when it's over a limit, and let Android do the dirty TextView measurement work
You will want to implement Textwatcher and in the onTextChanged(...) method, call TextView's setEms(...) method, which will set the width of the TextView to the width of (n m's), where n is the input for setEms(...).
...
TextView tv = (TextView) findViewById(R.id.my_text_view);
tv.addTextChangedListener(this);
...
#Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
tv.setEms(count);
}
This is my idea:
You can measure the text trivially with .length() if its length is bigger than your desired limit, than cut put only the substring. But if you want to display entire text you can implement onClickListener() on that TextView which envoking will show entire text as Dialog or whatever.
Another approach which I think can be suitable is to create different layouts for different density screens and then add in your xml android:maxLines="your limit" android:textSize="some value" and android:maxLength="#integer/mylimit" (different limit and size for different density). So what remains is to measure the length and only show the substring (or default text)
Something like:
String myText = "bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla";
String clickForMore="...Click for more";
int limit = myActivity.this.getResources().getInteger(R.integer.limit); //dont forget to declare it in your string.xml
Imagine we have 3 lines of text and we permitted only 2 (with total length value of int limit). Slice of our text fits inside the TextView, but it is not entirely shown. We need to inform the user that there is more to read...
myTextView.setVisibility(View.INVISIBLE);// <--- takes the space in the container
if(clickForMore.length() < myText.length() && myText.length() > limit)// <--- avoiding negative place and checking if the entire text is inside the box or there are characters leftover.
{
String temp = myText.substring(0, limit-clickForMore.length());// <-- we cutting space from currently displayed (not original string) to append the `clickForMore` string information.
myTextView.setText(temp+clickForMore);
myTextView.setVisibility(View.VISIBLE);
myTextView.setOnTouchListener(new OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
// display the dialog with full text....
return false;
}
}); //<--- instantiate the click listener
}
else
{
//the text fits the box and no need to slice it, just display it straight forward
}
myText.length() > limit - Test whether the string overlaps the limit.
Implement .addTextChangeListener() (TextWatcher) and in afterTextChange() override method implement all these I mentioned above.
Then you create onClickListener() for the TextView where you make dialog and then show your text as it is.
I hope you find this idea, reasonable. If you need more explanation what I had in mind feel free to write me.

How to correctly use TextSwitcher in ListView?

My TextSwitcher for each record in ListView should display first value (text1) and then another value (text2), then first value again and so on. It should happen only if text2 not empty. Otherwise text1 should be always shown (without any changes and animation).
I've created Runnable(), which changes boolean variable (time2) to then call items.notifyDataSetChanged(). It works as expected and in result setViewValue() for my ListView is called.
Here is the code:
items.setViewBinder(new SimpleCursorAdapter.ViewBinder() {
#Override
public boolean setViewValue(View view, Cursor cursor, int columnIndex) {
int viewId = view.getId();
switch(viewId) {
case R.id.timetext:
TextSwitcher itemTime = (TextSwitcher) view;
if (itemTime.getChildCount() != 2) {
itemTime.removeAllViews();
itemTime.setFactory(new ViewSwitcher.ViewFactory() {
#Override
public View makeView() {
TextView t = new TextView(MyActivity.this);
t.setTextSize(18);
t.setTypeface(null, Typeface.BOLD);
t.setTextColor(Color.WHITE);
return t;
}
});
itemTime.setAnimateFirstView(true);
itemTime.setInAnimation(AnimationUtils.loadAnimation(MyActivity.this,
R.anim.push_up_in));
itemTime.setOutAnimation(AnimationUtils.loadAnimation(MyActivity.this,
R.anim.push_up_out));
}
if (!text2.equals("")) {
if (!time2) {
itemTime.setText(text1);
} else {
itemTime.setText(text2);
}
} else {
itemTime.setCurrentText(text1);
}
return true;
}
return false;
}
} );
It works almost as expected. With one minor item - when text2 should be shown, it changes displayed value to some other value first (from another record!) and then animation is played. Change of text2 to text1 happens correctly.
My understanding that the reason is the following - before displaying text2, all views of itemTime are removed and hence it is recreated and that is why some other value is shown for a second. But why does it show value from some other record?
Actually text2 and text1 are values from the database, for ex.
text2 = cursor.getString(cursor.getColumnIndexOrThrow(DbAdapter.KEY_TIME_2)), probably, something is wrong here and setViewValue called with wrong parameters?
Upd. text1 and text2 are read from the database at setViewValue. Here is example of the full code:
itemTime.setText(cursor.getString(cursor.getColumnIndexOrThrow(DbAdapter.KEY_CLOSE_TIME_1)) + " - " + cursor.getString(cursor.getColumnIndexOrThrow(DbAdapter.KEY_OPEN_TIME_1)));
I know this might not answer the question directly, but I'm going to respond to your comment about creating a Runnable() to do the work of switching for you because I suspect that it is probably messing with your data (hard to tell when you cant see the full code).
I advise you to use a ViewFlipper instead of a TextSwitcher. The reason for doing that is that once you added the TextView's inside your ViewFlipper, you can just set your flip interval and then start the flipping and it will do it automatically for you.
As simple as this:
/* Add your items to your ViewFlipper first */
myViewFlipper.setFlipInterval(1000); //time in millseconds
myViewFlipper.startFlipping();
In your current method that you described, when you call items.notifyDataSetChanged() you incur a huge performance hit because all items of your database are going to be re-read and your list will be "re-drawn" again. You should only do that if your actual data really changed rather than using it to switch between text that you already have and doesn't change from creation time.
As a nice surprise, you might notice that your problem goes away because you don't have to re-read everything from you DB again and reduces the chances of mix-up of item1 and item2 since you will only need to read them once when the row is created in your ListView
Just my 2 cents.
Let me know how it goes.
I think I see what's going on here, and it's because of the way ListView works.
ListView recycles all of its views internally so that you only have as many views created as can be displayed on the screen. However, this also means that when you bind values to a view in your setViewValue method, you are not always given the view that was in the same position in the list before.
Say you have three list items: itemA, itemB, itemC in that order. Each contains text1, text2, and text3 respectively at first.
When you call items.notifyDataSetChanged(), ListView recycles all those list items however it feels like, so you may get a new order of itemC, itemA, itemB; and the text would then read text3, text1, text2.
As a result, when you change the text of the first list item to "text2", you will in fact see "text3" change to "text2" instead of a transition from "text1" to "text2" like you are expecting.
Are text1 and text2 stored in the resources file (res/values/strings.xml)? If so, Android will sometimes confuse variables. Simply running Project > Clean on this project may fix the problem.
This worked for me :
myViewFlipper.setFlipInterval(1000);
myViewFlipper.startFlipping();

Categories

Resources