Flow textview around image - android

I've spent hours looking for answer and have really no idea how to solve it. So let's get down to business:
There is an image and a TextView and I need to flow the TextView around the ImageView like this:
First possible solution woult be to use https://github.com/deano2390/FlowTextView but it's not extending TextView so this library is not suitable for me for number of reasons.
Second solution would be to use LeadingMarginSpan.LeadingMarginSpan2 span but it affects on each paragraph for each n lines inside the text (like in this answer -> How to layout text to flow around an image), so I get smth like this:
But I wanted to set margin only for first n lines! Then I decided to implement LeadingMarginSpan.Standart and create a counter and increment it in getLeadingMargin(first: Boolean): Int function invocation. When the counter reach the desirable value, the function returns 0 as a margin width. And there is a fail again! Instead of filling the TextView lines, the text just moved left and didn't spread to the end of the view!
UPD: Yes, I've used onGlobalLayoutListener in here
Well, googling for another solution I found this answer https://stackoverflow.com/a/27064368/7218592
Ok, I've done everything as described and implemented the code:
//set left margin of desirable width
val params: RelativeLayout.LayoutParams = RelativeLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
params.leftMargin = holder.imageContainerHeight!!
params.addRule(RelativeLayout.BELOW, holder.mNumberAndTimeInfo!!.id)
holder.mCommentTextView!!.layoutParams = params
if (holder.commentTextViewOnGlobalLayoutListener != null)
holder.mCommentTextView!!.viewTreeObserver.removeOnGlobalLayoutListener(
holder.commentTextViewOnGlobalLayoutListener)
//add onGlobalLayoutListener
holder.mCommentTextView!!.viewTreeObserver.addOnGlobalLayoutListener(
if (holder.commentTextViewOnGlobalLayoutListener != null)
holder.commentTextViewOnGlobalLayoutListener
else CommentTextViewOnGlobalLayoutListener(holder,
SpannableString(HtmlCompat.fromHtml(
mView.getActivity(), commentDocument.html(), 0,
null, SpanTagHandlerCompat(mView.getActivity())))))`
My OnGlobalLayoutListener looks like this: `
class CommentTextViewOnGlobalLayoutListener(
val holder: CommentAndFilesListViewViewHolder, val commentSpannable: Spannable) :
ViewTreeObserver.OnGlobalLayoutListener {
val LOG_TAG: String = CommentTextViewOnGlobalLayoutListener::class.java.simpleName
override fun onGlobalLayout() {
holder.mCommentTextView!!.viewTreeObserver.removeGlobalOnLayoutListener(this)
//when textview layout is drawn, get the line end to spanify only the needed text
val charCount = holder.mCommentTextView!!.layout.getLineEnd(Math.min(
holder.mCommentTextView!!.layout.lineCount - 1,
CommentLeadingMarginSpan.computeLinesToBeSpanned(holder)))
if (charCount <= commentSpannable.length) {
commentSpannable.setSpan(CommentLeadingMarginSpan(holder),
0, charCount, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
//set the left margin back to zero
(holder.mCommentTextView!!.layoutParams as RelativeLayout.LayoutParams).leftMargin = 0
holder.mCommentTextView!!.text = commentSpannable
}
}
`
Well, it works. But how terrible it works! As I'm using view holder pattern I have to hold a variable to the listener and remove if it is not been called and successfully removed because onGlobalLayout function wasn't called in time! And it is called too late, so you need to wait about 300 ms and then watch all the "reconstruction" of the TextView and it looks disgustingly!
So, my question is:
How to make margins for first n lines in TextView, before it's been drawn on UI?

This is more a suggestion that will only work with a little trial and error
This code uses a multi line Edit Text
btnPrint.setOnClickListener {
val str = """
One
Two
Three
Now click Action Button Custom SB
""".trimIndent()
etNews.setText(str)
}
Play with the One Two values indent and trimIndent has other properties available

Related

RecyclerView - Continuous Columns Layout

I am trying to create a layout where items would follow one another in columns (see image below) but I am not getting there yet. I have tried GridLayoutManager and StaggeredGridLayoutManager - the problem with both neither provides the feature of item flowing into another column and following each other this way. With my current attempt I am trying FlexboxLayoutManager but the result I am getting is always columns with single items instead of the items flowing one after another.
The desired behavior is that the items are located one after another and when the high of the recycler doesn't allow for the full item view it should be broken down to the next column.
Here is what I am trying right now:
mBinding?.activeRecycler?.layoutManager = FlexboxLayoutManager(context).apply {
flexDirection = FlexDirection.COLUMN
flexWrap = FlexWrap.WRAP
alignItems = AlignItems.STRETCH
}
And this is getting me one item per column.
Trying to achieve this:
I highly doubt this is possible.
The RecyclerView, its adapters and its layout managers all are not designed to alter the fundamental form of a view.
Meaning that "splitting" one would not be possible.
The RecyclerView is designed to understand how many views are in sight at the same time, create that many views only and then bind the underlying objects to the views respectively.
Meaning the RecyclerView doesn't "Cut a View in half and displays its halves in different places".
The only way in which a constellation like yours would be possible, was if the layout manager is specifically designed to display one item in multiple views and thereby multiple positions. Which would then allow it to be displayed as you described. However, as I said, that would mean the view 3 in the middle and the view 3 in the last column would be two views being bound to the same object or a copy of it. (Or someone went completely crazy and actually split the view, which I doubt).
I don't believe that any of the standard layout managers are capable of it and I doubt that you can even achieve this without also altering the adapter accordingly, at the very least. Because the adapter basically does the binding so without its help the standard layout managers wouldn't be able to do the double binding as described above.
That being said, this is just a very good guess, going by the principles of the view and its components. I have not read the source code or full description of every layout manager.
The way I understand your problem is like this: You have your current list of data that contains the text fields and you want to show them on the normal way, one list item one view item in recycler view.
But based on your design requirements this is not possible.
My idea to achieve that is like this:
You have to create a new list which will separate one item of the previous list into 2,3 or more items to fit in your columns.
private fun demo() {
val originalList = listOf<String>()
val newScreenSpecificList = mutableListOf<String>()
val columnHeight = 3//example number of lines
val columnWidth = 10//example number of chars
var columnsIndex = 0//index of column
var currentColumnHeight = 0 // current column filled height
originalList.forEach {
if (currentColumnHeight + getTextHeight(it, columnWidth) <= columnHeight) {
newScreenSpecificList.add(it)
currentColumnHeight = currentColumnHeight + getTextHeight(it, columnWidth)
} else {
//here is the part where your text is bigger then your column height so you need to divide it
val textForSpaceLeft = getTextForSpaceLeft(it, columnHeight - currentColumnHeight)
newScreenSpecificList.add(textForSpaceLeft)
currentColumnHeight = currentColumnHeight + getTextHeight(textForSpaceLeft, columnWidth)
if (currentColumnHeight >= columnHeight) {
columnsIndex++
}
if (getTextForNewSpaceLeft(it, columnHeight - currentColumnHeight)){
//continue to repeat logic for new column
//...
}
}
if (currentColumnHeight >= columnHeight) {
columnsIndex++
}
}
}
private fun getTextForSpaceLeft(it: String, spaceLeft: Int): String {
return "it"// return text for the available space
}
private fun getTextForNewSpaceLeft(it: String, spaceLeft: Int): String {
return "new column also"// return text left for the new available space
}
private fun getTextHeight(text: String, columnWidth: Int): Int {
return 2//todo your logic to convert text length to number of lines needed for a specific width of the column
}
Now you need to continue this logic it is not complete, I hope it helps you.
I guess your problem is with the LayoutParams of items which are being created in your adapter. probably the height is set to match_parent in items. You can try to change the LayoutParams of itemViews in your adapter's onCreateViewHolder/onBindViewHolder. Or if the items' heights are kinda tricky to calculate, you can create a customView and try calculate the height in onMeasure and set the height to wrap_content
try to set items' height to wrap_content or if you want to do it in code, something like this:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FlexItemViewHolder {
val infatedView = ...
infatedView.layoutParams = FlexboxLayoutManager.LayoutParams(FlexboxLayoutManager.LayoutParams.WRAP_CONTENT, FlexboxLayoutManager.LayoutParams.WRAP_CONTENT)
infatedView.addView(textView)
return FlexItemViewHolder(f)
}

Weighted buttons in vertical linear layout cutting off text if button text line counts vary

TL;DR: Here's the gist of everything that I can think of that's relevant to the issue I'm facing: [GIST LINK]
And here's a picture of the problem
I'm trying to set up a number of buttons that will all grow to the same size as each other by equal weighting in a vertically oriented LinearLayout container.
The problem I'm facing surfaces when the text on these buttons cause a different number of lines per button.
Let's say n is the lowest line count for the buttons and m is the highest line count; any descenders in the text of buttons with line count m are cut off. Refer to the words "qshowing my clipping problem" in the linked screengrab, where all descenders are cut off.
How can I go about fixing this? The clipping gets much worse if I introduce android:lineSpacingExtra to the button style.
If it's relevant, my minimum API is set to 21
I've fixed this using RxJava to set the height programmatically to the correct maximum so that no clipping occurs. If there is a better solution I'll be glad to see it, but this is what is working for me for now:
class MyActivity {
// ...
private val compositeDisposable = CompositeDisposable()
override fun onCreate(savedInstanceState: Bundle?) {
setContentView(R.layout.my_activity)
// ...
val container: LinearLayout = findViewById(R.id.container)
val numBtns = getNumBtnsToAdd()
val btnList: MutableList<Button> = mutableListOf()
val margin10 = dpToPx(10f).toInt()
val countDown = CountDownLatch(numBtns)
val desiredLp = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0).apply {
gravity = Gravity.CENTER
setMargins(margin10, margin10, margin10, margin10)
}
// Completable will be run once per subscriber and emit a success or error
val layoutCompletable = Completable.fromAction {
countDown.await()
for (btn in btnList) btn.layoutParams = desiredLp
}.subscribeOn(Schedulers.newThread()).observeOn(AndroidSchedulers.mainThread())
compositeDisposable.add(
layoutCompletable.subscribe({
Log.d("MyActivity", "Set LayoutParams on all buttons.")
}, Throwable::printStackTrace)
)
for (i in 0 until numBtns) {
val btn = Button(this, null, 0, R.style.button_style).apply {
layoutParams = LinearLayout.LayoutParams(desiredLp).apply { height = LinearLayout.LayoutParams.WRAP_CONTENTS }
text = if (i == 0) "Button${i+1} with short text"
else "Button${i+1} with text that will span multiple lines showing my clipping problem"
setOnClickListener { doSomething() }
}
val listener = object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
countDown.countDown()
val height = btn.height
if (height > desiredLp.height) desiredLp.height = height
btn.viewTreeObserver.removeOnGlobalLayoutListener(this)
}
}
btn.viewTreeObserver.addOnGlobalLayoutListener(listener)
btnList.add(btn)
container.addView(btn)
}
// ...
}
override fun finish() {
compositeDisposable.clear()
super.finish()
}
// ...
}
My guess, the main cause is buttons have fixed size. More preciously, you use LinearLayout to share available room between buttons via weight attribute. You can see the single line button height is same with 2-lines button height. So 2-lines buttons are forced to clip the text.
According your XML file you want enable the vertical scroll when there is no more room. In this case you don't need to use weight attribute. Just buttons under each other with margins.

Setting margins programmatically wont let me reassign the value each time

I'm trying to set up a layout at runtime that alternates from having a button on the left side of the screen on then the right side. I have an arrayList of button strings and it iterates through, creating a button for each and then applying some styling. Almost all the styling works, except that the margins I'm using to push them to the side of the screen aren't alternating correctly. I'm trying to make the margin push either from the left or the right, but it seems like the buttons are staying within only one column.
Here's the code first of all:
LayoutParams noteStyle = new LayoutParams((int) getResources().getDimension(R.dimen.sticky_note_height),
(int) getResources().getDimension(R.dimen.sticky_note_width));
int margin = (int) getResources().getDimension(R.dimen.margin_huge);
layout = (LinearLayout) findViewById(R.id.note_layout);
int i = 0;
for (String note : notes){
Button btnTag;
if (i % 2 == 0){
btnTag = (Button) getLayoutInflater().inflate(R.layout.sticky_note_right, null);
noteStyle.setMargins(margin,0,0,0);
} else {
btnTag = (Button) getLayoutInflater().inflate(R.layout.sticky_note_left, null);
noteStyle.setMargins(0,0,margin,0);
}
btnTag.setLayoutParams(noteStyle);
btnTag.setText(note);
btnTag.setId(i);
layout.addView(btnTag);
((Button) findViewById(i)).setOnClickListener(this);
i++;
}
And here's a screenshot of how it comes out:
For some reason unknown to me, reusing the LayoutParams can cause goofy results. Instantiating them each time they are needed can help resolve this.
This means putting them inside the for loop, in this situation
for (String note : notes) {
LayoutParams noteStyle = new LayoutParams((int) getResources().getDimension(R.dimen.sticky_note_height),
(int) getResources().getDimension(R.dimen.sticky_note_width));

Titanium add multiple views to a scrollview at once?

Is it possible to add multiple views to a ScrollView in titanium? So lets say I have the following:
var scrollView = Titanium.UI.createScrollView();
var views = [];
var view1 = Titanium.UI.createView();
views.push(view1);
var view2 = Titanium.UI.createView();
views.push(view2);
scrollView.add(views);
window.add(scrollView);
Would the above work? If not what needs to be done in order for it to work?
According to their documentation this shouldn't work. (Never tried before.) But you could do something like this:
views.forEach(function(view) {
scrollView.add(view);
});
I know this is a bit late, but its the solution I used to this problem, hopefully its still helpful. Basically, it involves setting the opacity of the scrollView to 0 until you are done loading. This means that rather than the rows appearing 1 at a time, they all appear simultaneously, and that this can be run in the background while your program/user does other stuff. Please note that it only works if the scrollView is empty - its not a good solution for adding rows to a scrollView that already has stuff in it:
var sView = Titanium.UI.createScrollView({
//Whatever properties you need for your scrollView
opacity: 0,
});
//childViews is an array of all the stuff you'd like to add to sView
childCount = childViews.length
//Add a postlayout event to the last childView - this will automatically set the opacity to 1 when the last child is loaded
childViews[childCount - 1].addEventListener('postlayout', function showScrollView(e){
this.parent.setOpacity(1);
this.removeEventListener(showScrollView);
});
//Iteratively add each view in the array to the sView
for (var x = 0; x < childCount; x++) {
sView.add(childViews[x]);
}
window.add(sView);

When is the width of an Android View set?

I have a little issue on what sequence things are being called when adding stuff to a RelativeLayout. I have a class extending Activity (name it RelActivity) where I want to create a RelativeLayout and put several custom Views (name it cusView) into that RelativeLayout. The topMargin and leftMargin of a custom View is calculated by using the position of another custom View (i.e. the first custom View has to be positioned directly by setting a number to topMargin and leftMargin). Please note that the Rules of RelativeLayout is not sufficient in this case.
So, over to the problem. In my RelActivity I do this:
Create a RelativeLayout (name it relLayout)
Iterate a cursor with cusViews recieved from a database
For the first cusView -> Set position by topMargin and leftMargin using a LayoutParameter
For the other cusViews -> calculate their topMargin and leftMargin by using one of the other cusViews and a LayoutParameter
Set RelActivity's contentView to relLayout
What happens is that all cusViews but the first one are squeezed in the top left corner because both leftMargin and topMargin are always calculated to be zero. This happens because I use the width of the cusViews to calculate the topMargin and leftMargin, and the width of the cusView has not given a value yet.
Is the width first calculated in the cusView's overrided method onSizeChanged()? Is the onSizeChanged() method get called first when the layout is presented on the screen? If so, how do I work around this issue? Do I have to calculate the positionings after onSizeChanged() is done?
Edit: Here is a minimum working example:
Here is my onCreate in RelActivity:
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
relLayout = new RelativeLayout(this);
cusViews = new ArrayList<CusView>();
listParams = new ArrayList<RelativeLayout.LayoutParams>();
readDBandSetLayout();
setContentView(relLayout);
}
There is too much information in the readDBandSetLayout() method to present it all here. below are the most important details. If I create the LayoutParams in the following way it works fine, the cusViews are listed downwards and rightwards of eachother:
queryCursor = customApplication.customData.query( number); //Fetches cursor
for ( int i = 0; i < numberOfRows; i++ ){
if ( i == 0 ){
LayoutParams p = new LayoutParams(this.getResources().getDimensionPixelSize(R.dimen.small), this.getResources().getDimensionPixelSize(R.dimen.small));
p.topMargin = 50;
p.leftMargin = 50;
listParams.add(p);
}
else{
LayoutParams p = new LayoutParams(this.getResources().getDimensionPixelSize(R.dimen.large),this.getResources().getDimensionPixelSize(R.dimen.large));
p.addRule(RelativeLayout.BELOW, cusViews.get(i-1).getId());
p.addRule(RelativeLayout.RIGHT_OF, cusViews.get(i-1).getId());
listParams.add(p);
}
relLayout.addView(cusViews.get(i), listParams.get(i));
}
However, what I want to do in the else statement is something like:
else{
LayoutParams p = new LayoutParams(this.getResources().getDimensionPixelSize(R.dimen.large),this.getResources().getDimensionPixelSize(R.dimen.large));
//Here I want to calculate cusView2Topmargin and cusView2Leftmargin based on the widths of the first or previosly positioned cusViews. But here the widths are 0 since they haven't been calculated yet.
p.topMargin = cusView2Topmargin; //Always zero
p.leftMargin = cusView2Leftmargin; //Always zero
listParams.add(p);
}
So the problem lies in that the widths of the cusViews are zero at the point I need them to calculate the layout parameters topMargin and leftMargin.
Unfortunately I cannot use the RelativeLayout's Rules for what I want to achieve. If there were some way to create rules like RelativeLayout.RIGHT_OF and RelativeLayout.BELOW I could do it like that. Is this possible?
Its not very clear what your goal is for this layout. It might well be possible to use a simple LinearLayout to get what you want.
If you want to size these from a database lookup then try simply adding each of the views, using addView() first, storing a reference to each, then go back and sett the margins to place them in the proper positions.

Categories

Resources