Align NumberPicker Text to Left - android

I am using custom string in NumberPicker. The problem is the text comes in center, while I want it to align to the Left.
I tried extracting the TextView and setting the gravity there, using the following code -
TextView npTextView = (TextView) numberPicker.getChildAt(1);
npTextView.setGravity(Gravity.LEFT);
But it sets the gravity of selected Text only. The text which is in scrolling position, still comes at center.
Is there any way to align the text to left?
PS: I am using API Level 15.

Eight years have passed since the time when the question was asked, so you could have +8 years of Android experience and may be capable of developing the desired number picker yourself :)
Whatever, I've encountered a similar problem and upgraded Android NumberPicker to fulfill my needs. The original NumberPicker is far from perfect, so is mine, but after some tuning it worked for me.
Usage example:
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View = LinearLayout(requireContext()).apply {
isBaselineAligned = false
val divider = ColorDrawable(0xFFDBDBDB.toInt()).apply {
// share drawable, don't mind Callback, invalidation etc
setBounds(0, 0, 0, dp2Px(context, 1)) // 1dp dividers
}
val pad = dp2Px(context, 16)
val selectorWheelPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
typeface = ResourcesCompat.getFont(context, R.font.roboto_light)
textSize = sp(22f)
}
val scaleI = TimeInterpolator { cos(3.1415926535897932384f / 2f * it) }
val alphaI = TimeInterpolator { cos(3.1415926535897932384f / 1.5f * it) }
val cmPicker = FlexibleNumberPicker(context).apply {
minValue = 0
maxValue = 1000 // 10 metres is enough, I hope
value = valueMm / 100
setup(divider, selectorWheelPaint, scaleI, alphaI)
setPadding(0, 0, pad, 0)
val cm = resources.getString(R.string.cm)
setFormatter { cm.format(it.toFloat()) }
wrapSelectorWheel = false
setGravity(Gravity.RIGHT)
}
addView(cmPicker, LinearLayout.LayoutParams(0, WRAP_CONTENT, 1f))
addView(FlexibleNumberPicker(context).apply {
minValue = 0
maxValue = 9
value = valueMm % 100
setup(divider, selectorWheelPaint, scaleI, alphaI)
setPadding(pad, 0, 0, 0)
val mm = resources.getString(R.string.mm)
setFormatter { mm.format(it.toFloat()) }
setOnValueChangedListener(cmPicker.fractionListener)
setGravity(Gravity.LEFT)
}, LinearLayout.LayoutParams(0, WRAP_CONTENT, 1f))
}
private fun FlexibleNumberPicker.setup(
divider: ColorDrawable, paint: Paint,
scaleI: TimeInterpolator, alphaI: TimeInterpolator,
) {
setSelectionDivider(divider)
setSelectorWheelPaint(paint)
setWheelItemCount(5)
setDividerOffset(.33f)
setLineHeight(TypedValue.COMPLEX_UNIT_SP, 27f)
solidColor = Color.WHITE
setScaleInterpolator(scaleI)
setAlphaInterpolator(alphaI)
}
val FlexibleNumberPicker.fractionListener: FlexibleNumberPicker.OnValueChangeListener
get() = FlexibleNumberPicker.OnValueChangeListener { picker, oldVal, newVal ->
val min = picker.minValue
val max = picker.maxValue
when {
oldVal == min && newVal == max -> changeValueByOne(false)
oldVal == max && newVal == min -> changeValueByOne(true)
}
}

You can't be sure that 2nd child of NumberPicker is the right TextView because implementation of NumberPicker in different APIs is different. Try this code instead.
for (int i = 0; i < numberPicker.getChildCount(); i++) {
View child = numberPicker.getChildAt(i);
if (child instanceof EditText) {
((EditText) child).setGravity(Gravity.LEFT);
break;
}
}

Related

Kotlin how get width of wrap_content TextView

I have a problem, because I need to have width of my TextView. I have already width of my Layout, but I need to have a width of specific element too. In this case TextView. I'm trying to get it, but I think that addOnLayoutChangeListener is going on another scope or sth because when I try to assign width to var textWidth I can't do this and variable return 0, but in println I can see that there is a value which I need. How can I get this value?
var textWidth = 0
textViewOne.addOnLayoutChangeListener {
v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom
-> textWidth = right-left
println("${right-left}") <-- this return 389
}
println("${textWidth}") <-- this return 0
Any tips how to do take width of TextView?
I believe the views dimensions are not evaluated at this stage so you don’t have a choice but to wait until the layout is fully rendered.
Assuming you are not measuring a single view, I’d recommend attaching OnGlobalLayoutListener to the root view:
rootView.viewTreeObserver.addOnGlobalLayoutListener {
// Do your thing
}
And if you want the code to be executed only once:
rootView.viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
override fun onGlobalLayout() {
rootView.viewTreeObserver.removeOnGlobalLayoutListener(this)
// Do your thing
}
})
I just solved problem if anyone need solution this workes for me:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val displayMetrics: DisplayMetrics = applicationContext.resources.displayMetrics
val pxWidth = displayMetrics.widthPixels
val baseLayout = findViewById<LinearLayout>(R.id.baseLayout)
baseLayout.doOnLayout {
val textViewOne = findViewById<TextView>(R.id.textVieOne)
val oneWidth = textViewOne.width
val textViewTwo = findViewById<TextView>(R.id.textViewTwo)
val twoWidth = textViewTwo.width
val textViewThree = findViewById<TextView>(R.id.textViewThree)
val threeWidth = textViewThree.width
val sumOfChildWidths = oneWidth + twoWidth + threeWidth
if(pxWidth <= sumOfChildWidths){
textViewThree.isVisible = false
}
}
}

Placing dynamically added buttons below each other

I'm building a calculator app and in it there's a ScrollView, used to show and switch the buttons for operators and units whenever the user switches between modes.
The problem is that I didn't want to create a XML layout for each mode, so I thought of adding those buttons programmatically (which now, for me, seems pretty hard to accomplish). Here's the code that's supposed to add them:
// For each text in texts (which represents the modes), a Button is created and, if its id (represented by the index of its respective text in the list) is greater than 0, it is positioned below the previous one.
fun ScrollView.add(context: Context, input: EditText, texts: List<String>) {
removeAllViews()
val container = ConstraintLayout(context).apply {
layoutParams = ConstraintLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
topToTop = this#add.top
startToStart = this#add.left
endToEnd = this#add.right
}
}
val buttons: MutableList<Button> = mutableListOf()
texts.forEach { text ->
val button = Button(context)
val originalWidth = 60
val width = originalWidth.toDimension(context, COMPLEX_UNIT_DIP)
val originalHeight = 60
val height = originalHeight.toDimension(context, COMPLEX_UNIT_DIP)
with(button) {
layoutParams = ConstraintLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
id = texts.indexOf(text)
val previous = try { buttons[id - 1] } catch (exception: Exception) { this }
setWidth(width)
setHeight(height)
with(layoutParams as ConstraintLayout.LayoutParams) {
if (id == 0)
topToTop = this#add.top
else if (id > 0) {
layoutParams = RelativeLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
(layoutParams as RelativeLayout.LayoutParams).addRule(BELOW, previous.id)
}
}
left = this#add.left
right = this#add.right
button.text = text
isAllCaps = false
textSize = 25f
while (text().contains(System.getProperty("line.separator").toString())) textSize -= 5
setOnClickListener { input.input((it as Button).text()) }
setBackgroundResource(0)
container.addView(this)
buttons.add(this)
val buttonAdded = "[Dynamic List] Button $id added as \"$text\""
println(if (id == 0) "$buttonAdded." else "$buttonAdded. The button was positioned below \"${previous.text}\" (${previous.id}).")
}
}
addView(container)
}
And here's the code I implemented using the method above:
// Each calculator mode is represented by an Int within the list.
val modes = listOf("calculator" to 1, "length" to 2, "temperature" to 3, "time" to 4)
fun mode(context: Context, button: Button, input: EditText, view: ScrollView) {
var counter = 0
val operators = listOf("+", "-", times.toString(), division.toString())
val length = with(context) { listOf(getString(R.string.light_year), context.getString(R.string.kilometer), context.getString(R.string.hectometer), context.getString(R.string.decameter), context.getString(R.string.mile), context.getString(R.string.meter), context.getString(R.string.centimeter), context.getString(R.string.millimeter), context.getString(R.string.micrometer)) }
val temperature = with(context) { listOf(getString(R.string.celsius), context.getString(R.string.fahrenheit), context.getString(R.string.kevin), context.getString(R.string.rankine), context.getString(R.string.reaumur)) }
val time = with(context) { listOf(getString(R.string.year), context.getString(R.string.month), context.getString(R.string.day), context.getString(R.string.hour), context.getString(R.string.minute), context.getString(R.string.second), context.getString(R.string.millisecond)) }
with(button) {
setOnClickListener {
if (counter < modes.size - 1) counter++ else counter = 0
with(view) {
val mode: Pair<String, Int>? = modes[counter]
when (mode?.first) {
"calculator" -> add(context, input, operators)
"length" -> add(context, input, length)
"temperature" -> add(context, input, temperature)
"time" -> add(context, input, time)
}
text = with(context) {
with(resources) { getString(identify(context, mode?.first, "string")) }
}
}
}
}
}
Well, the problem is when I run it, the UI ends up looking like this, with all the buttons positioned at the same place:

Setting maximum expanded height for bottomsheet dynamically

How to set maximum expanded height in android support design bottom sheet?
The question is an extension to the above question, i want to set the max expanded height of the sheet but dynamically according to the screen size.
I have tried setting new layout params to the view implementing bottomsheet behaviour but it does nothing good.
Please use this and chill :)
const val BOTTOMSHEET_HEIGHT_TO_SCREEN_HEIGHT_RATIO = 0.80 //change according to your requirement
override onCreateDialog() in your bottomsheetFragment
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState) as BottomSheetDialog
dialog.setOnShowListener {
dialog.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet)
?.apply {
val maxDesiredHeight =
(resources.displayMetrics.heightPixels * BOTTOMSHEET_HEIGHT_TO_SCREEN_HEIGHT_RATIO).toInt()
if (this.height > maxDesiredHeight) {
val bottomSheetLayoutParams = this.layoutParams
bottomSheetLayoutParams.height = maxDesiredHeight
this.layoutParams = bottomSheetLayoutParams
}
BottomSheetBehavior.from(this)?.apply {
this.state = BottomSheetBehavior.STATE_EXPANDED
this.skipCollapsed = true
}
}
}
return dialog
}
2021
I'm late but someone will need
Kotlin extenxion:
fun View.setupFullHeight(maxHeight: Double = 0.3) {
val displayMetrics = context?.resources?.displayMetrics
val height = displayMetrics?.heightPixels
val maximalHeight = (height?.times(maxHeight))?.toInt()
val layoutParams = this.layoutParams
maximalHeight?.let {
layoutParams.height = it
}
this.layoutParams = layoutParams
}
How to use:
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return object : BottomSheetDialog(requireContext(), R.style.DialogRoundedCornerStyle) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
dialog?.setOnShowListener {
val bottomSheetDialog = it as BottomSheetDialog
val parentLayout =
bottomSheetDialog.findViewById<View>(R.id.design_bottom_sheet)
parentLayout?.let { view ->
val behavior = BottomSheetBehavior.from(view)
view.setupFullHeight()
behavior.apply {
state = BottomSheetBehavior.STATE_EXPANDED
isDraggable = false
isCancelable = false
}
}
}
}
override fun onBackPressed() {
super.onBackPressed()
dialog?.dismiss()
}
}
}
The simplest solution is to set the maxHeight property of the bottom sheet like this.
DisplayMetrics displayMetrics = new DisplayMetrics();
activity.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
bottomSheet.setMaxHeight((int) (displayMetrics.heightPixels * 0.65));
Finally found it,
This question troubled me a lot with no solution reported anywhere, and the answer lies in the behavior itself.
The minimum offset is the max value upto which the bottomsheet should move and we set the lower cap of the value to our desired height upto which we want the bottomsheet to move.
You can expose a function to set the value or do it direclty in our behavior.
To dynamically set the max expanded height for bottomsheet we need to increase the minimum offset value from 0 to our desired value in BottomSheetBehavior class, let me show the code.
Happy coding!!
// The minimum offset value upto which your bottomsheet to move
private int mMinOffset;
/**
* Called when the parent CoordinatorLayout is about the layout the given child view.
*/
#Override
public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
int dynamicHeight = Utils.dpToPx(parent.getContext(), **your_value_in_dp**);
mMinOffset = Math.max(dynamicHeight, mParentHeight - child.getHeight());
mMaxOffset = Math.max(mParentHeight - mPeekHeight, mMinOffset);
mAnchorOffset = Math.min(mParentHeight - mAnchorHeight, mMaxOffset);
if (mState == STATE_EXPANDED) {
ViewCompat.offsetTopAndBottom(child, mMinOffset);
anchorViews(mMinOffset);
}
}

Call getMeasuredWidth() or getWidth() for RecyclerView return 0 on data binding

I'm using data binding to setup a RecyclerView. Here is the binding adapter:
fun setRecyclerDevices(recyclerView: RecyclerView, items: List<Device>, itemBinder: MultipleTypeItemBinder,
listener: BindableListAdapter.OnClickListener<Device>?) {
var adapter = recyclerView.adapter as? DevicesBindableAdapter
if (adapter == null) {
val spannedGridLayoutManager = SpannedGridLayoutManager(orientation = SpannedGridLayoutManager.Orientation.VERTICAL,
spans = getSpanSizeFromScreenWidth(recyclerView.context, recyclerView))
recyclerView.layoutManager = spannedGridLayoutManager
recyclerView.addItemDecoration(SpaceItemDecorator(left = 15, top = 15, right = 15, bottom = 15))
adapter = DevicesBindableAdapter(items, itemBinder)
adapter.setOnClickListener(listener)
recyclerView.adapter = adapter
} else {
adapter.setOnClickListener(listener)
adapter.setItemBinder(itemBinder)
adapter.setItems(items)
}
}
getSpanSizeFromScreenWidth needs the recycler's width to do some calculation. But it always returns 0.
I tried to apply a ViewTreeObserver like this:
recyclerView.viewTreeObserver.addOnGlobalLayoutListener(object: ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
recyclerView.viewTreeObserver.removeOnGlobalLayoutListener(this)
val spannedGridLayoutManager = SpannedGridLayoutManager(orientation = SpannedGridLayoutManager.Orientation.VERTICAL,
spans = getSpanSizeFromScreenWidth(recyclerView.context, recyclerView))
recyclerView.layoutManager = spannedGridLayoutManager
}
})
Or use post like this:
recyclerView.post({
val spannedGridLayoutManager = SpannedGridLayoutManager(orientation = SpannedGridLayoutManager.Orientation.VERTICAL,
spans = getSpanSizeFromScreenWidth(recyclerView.context, recyclerView))
recyclerView.layoutManager = spannedGridLayoutManager
})
Code of getSpanSizeFormScreenWidth:
private fun getSpanSizeFromScreenWidth(context: Context, recyclerView: RecyclerView): Int {
val availableWidth = recyclerView.width.toFloat()
val px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 300f, context.resources.displayMetrics)
val margin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, 15f, context.resources.displayMetrics)
return Math.max(1, Math.floor((availableWidth / (px + margin)).toDouble()).toInt()) * DevicesBindableAdapter.WIDTH_UNIT_VALUE
}
But it still returns 0 despite my RecyclerView being displayed on the screen (not 0).
Any ideas?
In inspecting the code, it appears that your RecyclerView may actually be fine, but your logic in getSpanSizeFromScreenWidth may not be.
It looks like this: Math.floor((availableWidth / (px + margin)).toDouble()).toInt() will always be 0 when availableWidth is less than (px + margin). This will then cause getSpanSizeFromScreenWidth to return 0.
Breaking it down:
Math.floor - rounds a double down to a whole number
availableWidth / (px + margin) - will be a low number (a fraction of availableWidth)
Therefore, you're going to get 0 at times especially on smaller screens and/or smaller density screens.
Does that make sense? May not be this issue, but I'd start there. It's hard to tell you exactly the issue without knowing the whole context, but that's likely your issue.
If that is not your issue, could you say what your value is for availableWidth, px, and margin during execution?

How to use custom ellipsis in Android TextView

I have a TextView with maxlines=3 and I would like to use my own ellipsis, instead of
"Lore ipsum ..."
I need
"Lore ipsum ... [See more]"
in order to give the user a clue that clicking on the view is going to expand the full text.
Is it possible ?
I was thinking about check whether TextView has ellipsis and in such a case add the text "[See more]" and after that set ellipsis just before, but I couldn't find the way to do that.
Maybe if I find the position where the text is cutted, I can disable the ellipsis and make a substring and later add "... [See more]", but again I dont know how to get that position.
I've finally managed it in this way (may be not the best one):
private void setLabelAfterEllipsis(TextView textView, int labelId, int maxLines){
if(textView.getLayout().getEllipsisCount(maxLines-1)==0) {
return; // Nothing to do
}
int start = textView.getLayout().getLineStart(0);
int end = textView.getLayout().getLineEnd(textView.getLineCount() - 1);
String displayed = textView.getText().toString().substring(start, end);
int displayedWidth = getTextWidth(displayed, textView.getTextSize());
String strLabel = textView.getContext().getResources().getString(labelId);
String ellipsis = "...";
String suffix = ellipsis + strLabel;
int textWidth;
String newText = displayed;
textWidth = getTextWidth(newText + suffix, textView.getTextSize());
while(textWidth>displayedWidth){
newText = newText.substring(0, newText.length()-1).trim();
textWidth = getTextWidth(newText + suffix, textView.getTextSize());
}
textView.setText(newText + suffix);
}
private int getTextWidth(String text, float textSize){
Rect bounds = new Rect();
Paint paint = new Paint();
paint.setTextSize(textSize);
paint.getTextBounds(text, 0, text.length(), bounds);
int width = (int) Math.ceil( bounds.width());
return width;
}
I think the answer from #jmhostalet will degrade the performance (especially when dealing with lists and lots of TextViews) because the TextView draws the text more than once. I've created a custom TextView that solves this in the onMeasure() and therefore only draws the text once.
I've originally posted my answer here: https://stackoverflow.com/a/52589927/1680301
And here's the link to the repo: https://github.com/TheCodeYard/EllipsizedTextView
Here's a nice way to do it with a Kotlin extension.
Note that we need to wait for the view to layout before we can measure and append the suffix.
In TextViewExtensions.kt
fun TextView.setEllipsizedSuffix(maxLines: Int, suffix: String) {
addOnLayoutChangeListener(object: View.OnLayoutChangeListener {
override fun onLayoutChange(v: View?, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) {
val allText = text.toString()
var newText = allText
val tvWidth = width
val textSize = textSize
if(!TextUtil.textHasEllipsized(newText, tvWidth, textSize, maxLines)) return
while (TextUtil.textHasEllipsized(newText, tvWidth, textSize, maxLines)) {
newText = newText.substring(0, newText.length - 1).trim()
}
//now replace the last few chars with the suffix if we can
val endIndex = newText.length - suffix.length - 1 //minus 1 just to make sure we have enough room
if(endIndex > 0) {
newText = "${newText.substring(0, endIndex).trim()}$suffix"
}
text = newText
removeOnLayoutChangeListener(this)
}
})
}
In TextUtil.kt
fun textHasEllipsized(text: String, tvWidth: Int, textSize: Float, maxLines: Int): Boolean {
val paint = Paint()
paint.textSize = textSize
val size = paint.measureText(text).toInt()
return size > tvWidth * maxLines
}
Then actually using it like this
myTextView.setEllipsizedSuffix(2, "...See more")
Note: if your text comes from a server and may have new line characters, then you can use this method to determine if the text has ellipsized.
fun textHasEllipsized(text: String, tvWidth: Int, textSize: Float, maxLines: Int): Boolean {
val paint = Paint()
paint.textSize = textSize
val size = paint.measureText(text).toInt()
val newLineChars = StringUtils.countMatches(text, "\n")
return size > tvWidth * maxLines || newLineChars >= maxLines
}
StringUtils is from implementation 'org.apache.commons:commons-lang3:3.4'
#George #jmhostalet i was doing this in my recycler view and it degraded the whole performance. `
ViewTreeObserver vto = previewContent.getViewTreeObserver();
vto.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
#Override
public boolean onPreDraw() {
try {
Layout layout = previewContent.getLayout();
int index1 = layout.getLineStart(previewContent.getMaxLines());
if (index1 > 10 && index1 < ab.getPreviewContent().length()) {
String s =
previewContent.getText().toString().substring(0, index1 - 10);
previewContent
.setText(Html.fromHtml(
s + "<font color='#DC5530'>...और पढ़ें</font>"));
}
return true;
}catch (Exception e)
{
Crashlytics.logException(e);
}
return true;
}
});`
Here is a solution for Kotlin.
The yourTextView.post{} is necessary because the textview won't be ellipsized until after it is rendered.
val customSuffix = "... [See more]"
yourTextView.post {
if (yourTextView.layout.getEllipsisStart(-1) != -1) {
val newText = yourTextView.text.removeRange(
yourTextView.layout.getEllipsisStart(-1) - customSuffix.length, yourTextView.text.length
)
yourTextView.text = String.format("%s%s", newText, customSuffix)
}
}

Categories

Resources