I have used a custom toolbar class so i can align the title to the right and every thing works fine except the navigation back icon is not vertically aligned
and this is the custom toolbar class
class RTLToolbar #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : Toolbar(context, attrs, defStyleAttr) {
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
super.onLayout(changed, l, t, r, b)
val childCount = childCount
for (i in 0 until childCount) {
val view = this.getChildAt(i)
if (view is TextView) {
forceTitleCenter(view,l, r)
break
}
}
}
private fun forceTitleCenter(view: TextView, l: Int, r: Int) {
val top = view.top
val bottom = view.bottom
view.layout(l, top, r, bottom)
navigationIcon?.let{ view.setPadding(it.intrinsicWidth,0,0,0) }
view.gravity = Gravity.RIGHT
}
}
i found this xml attribute
app:buttonGravity="center_vertical"
and it did the job, now the back icon is aligned with the title
Related
Using a Compose view that inherent from AbstractComposeView
inside an XML ui code of a fragment
Knowing that this fragment is part of a navigation graph (Jetpack navigation)
When i press the back button going back to my fragment, the compose view just disappeared.
It's only drawing for the first time i open the fragment.
Bellow view code
class ProgressComposeView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AbstractComposeView(context, attrs, defStyleAttr) {
private var steps = mutableStateOf(0)
private var currentStep: Int = 0
private var windowWidth: Int = 0
#Composable
override fun Content() {
ProgressView(steps.value, currentStep, windowWidth)
}
fun setData(steps: Int, currentStep: Int, windowWidth: Int) {
this.steps.value = steps
this.currentStep = currentStep
this.windowWidth = windowWidth
}
}
#Composable
fun ProgressView(totalSteps: Int, currentStep: Int, windowWidth: Int) {
..... }
Solution :
You have to call .disposeComposition() on your ComposeView
In your onResume() function
Example :
override fun onResume() {
super.onResume()
binding.composeHeader.disposeComposition()
}
In Android UI, we can create a custom view by overloading View as shown below.
class CustomView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0)
: View(context, attrs, defStyleAttr) {
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// Perform the needing drawing
if (isAttachedToWindow) invalidate()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val desiredWidth = suggestedMinimumWidth + paddingLeft + paddingRight
val desiredHeight = suggestedMinimumHeight + paddingTop + paddingBottom
setMeasuredDimension(View.resolveSize(desiredWidth, widthMeasureSpec),
View.resolveSize(desiredHeight, heightMeasureSpec))
}
}
Can we wrap JetpackCompose in this CustomView, so that the underlying Drawing it using JetpackCompose instead?
I check https://developer.android.com/jetpack/compose/interop/interop-apis, don't seems to have it stated.
To get it to work, we can have
class CustomComposeView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AbstractComposeView(context, attrs, defStyleAttr) {
#Composable
override fun Content() {
// JetpacCompose code here
}
}
In the XML, we can have something like this
<com.package.CustomComposeView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="700dp" />
I want to use a centered spinner where the width of the spinner is only as wide as the selected item text. From my research it seems that this is not natively supported out of the box with an attribute so I found another StackOverflow question/answer and tried implementing that but ran into some issues with it.
So I took option 1 from this SO response and implemented it in Kotlin and It's not working for me
class DynamicWidthSpinner #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatSpinner(context, attrs, defStyleAttr) {
override fun setAdapter(adapter: SpinnerAdapter?) {
super.setAdapter(if (adapter != null) WrapperSpinnerAdapter(adapter) else null)
}
inner class WrapperSpinnerAdapter(val baseAdapter: SpinnerAdapter) : SpinnerAdapter {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
return baseAdapter.getView(selectedItemPosition, convertView, parent)
}
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
return baseAdapter.getDropDownView(position, convertView, parent)
}
override fun getCount(): Int = baseAdapter.count
override fun getItem(position: Int): Any = baseAdapter.getItem(position)
override fun getItemId(position: Int): Long = baseAdapter.getItemId(position)
override fun getItemViewType(position: Int): Int = baseAdapter.getItemViewType(position)
override fun getViewTypeCount(): Int = baseAdapter.viewTypeCount
override fun hasStableIds(): Boolean = baseAdapter.hasStableIds()
override fun isEmpty(): Boolean = baseAdapter.isEmpty
override fun registerDataSetObserver(observer: DataSetObserver) {
baseAdapter.registerDataSetObserver(observer)
}
override fun unregisterDataSetObserver(observer: DataSetObserver) {
baseAdapter.unregisterDataSetObserver(observer)
}
}
}
and in my MainActivity I'm doing this from onCreate
val spinner: DynamicWidthSpinner = findViewById(R.id.global_toolbar_location_spinner)
val tempLocationList = ArrayList<String>()
tempLocationList.add("Test1")
tempLocationList.add("Much longer test string 2")
spinner.adapter = ArrayAdapter(
this,
R.layout.global_toolbar_spinner_item,
tempLocationList
)
spinner.onItemSelectedListener = object : OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, arg3: Long) {
// TODO: do stuff on selection here
}
override fun onNothingSelected(arg0: AdapterView<*>) {
// TODO: do nothing... yet
}
}
spinner.setSelection(0)
and I am using my custom Spinner in the layout xml (ommitting everything else that is not necessary because I am able to get it work just fine using the native <Spinner> or androidx compat Spinner
<com.blablabla.app.ui.DynamicWidthSpinner
android:id="#+id/global_toolbar_location_spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:spinnerMode="dialog"
/>
What I see is just the first item "Test1" and nothing happens when I tap on it and arrow seems to have disappeared now as well
I figured out the issue. Turns out that this is one of those cases where the #JvmOverloads doesn't work. Once I converted it to the multiple constructor kotlin syntax it worked without a problem
class DynamicWidthSpinner : AppCompatSpinner {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
...
}
I need to onDraw the items of a RecyclerView. Using an approach "discovered" at this SO link, I have gotten - um - partway there.
Note that I ultimately want to onDraw "over" the custom view. Meaning call super to let the default drawing occur, then paint over unused areas of the (view's) canvas.
Before starting down this "custom view to allow onDraw" road, I had what you see on the left below:
Afterward, I had all "invisible" views (middle image above). I say "invisible" because they were still there to be clicked. To help me visualize things (and a bit of onDraw proof of concept) I overrode onDraw in the custom PuzzleView view, simply calling canvas.drawRect to cover the entire canvas in Green, and now see the right image above.
I am not sure what I'm doing wrong here.
Also, if it occurs to you that - well, why don't I simply onDraw the whole thing - that's not practical for a variety of reasons.
So, here's my PuzzleAdapter as it is now:
class PuzzleAdapter(private val puzzles: List<Puzzle>) : RecyclerView.Adapter<PuzzleAdapter.PuzzleHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PuzzleAdapter.PuzzleHolder {
//val v = LayoutInflater.from(parent.context).inflate(R.layout.item_puzzle, parent, false)
//not inflating, creating PuzzleView (which is inflating itself)
val v = PuzzleView(parent.context)
v.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
return PuzzleHolder(v)
}
override fun getItemCount() = puzzles.size
//unchanged between these two versions
override fun onBindViewHolder(h: PuzzleAdapter.PuzzleHolder, pos: Int) {
val p = puzzles[pos]
h.view.puzzleItem_text_ndx.text = "# " + p.descr()
h.view.puzzleItem_text_details.text = "Ndx: ${p.puzzleNdx}"
h.view.setOnClickListener {
Log.d("##", "PuzzleHolder.onClick (bunch#${p.parentBunch.bunchID}; puzzle#${p.puzzleNdx})")
val action = BunchFragmentDirections.navBunchToPuzzle(PuzzleParcel(p.parentBunch.bunchID, p.puzzleNdx))
it.findNavController().navigate(action)
}
}
inner class PuzzleHolder(v: View) : RecyclerView.ViewHolder(v) {
val view: PuzzleView
init {
view = v as PuzzleView
}
}
}
PuzzleAdapter before:
class PuzzleAdapter(private val puzzles: List<Puzzle>) : RecyclerView.Adapter<PuzzleAdapter.PuzzleHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PuzzleAdapter.PuzzleHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.item_puzzle, parent, false)
return PuzzleHolder(v)
}
override fun getItemCount() = puzzles.size
//unchanged between these two versions
override fun onBindViewHolder(h: PuzzleAdapter.PuzzleHolder, pos: Int) {
val p = puzzles[pos]
h.view.puzzleItem_text_ndx.text = "# " + p.descr()
h.view.puzzleItem_text_details.text = "Ndx: ${p.puzzleNdx}"
h.view.setOnClickListener {
Log.d("##", "PuzzleHolder.onClick (bunch#${p.parentBunch.bunchID}; puzzle#${p.puzzleNdx})")
val action = BunchFragmentDirections.navBunchToPuzzle(PuzzleParcel(p.parentBunch.bunchID, p.puzzleNdx))
it.findNavController().navigate(action)
}
}
inner class PuzzleHolder(v: View) : RecyclerView.ViewHolder(v) {
var view: View = v
}
}
PuzzleView (the custom view I am using with the Adapter):
class PuzzleView : RelativeLayout {
constructor (context: Context) : super(context) { init(context, null, 0) }
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { init(context, attrs, 0) }
constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) { init(context, attrs, defStyle) }
private fun init(context: Context, attrs: AttributeSet?, defStyle: Int) {
inflate(getContext(), R.layout.item_puzzle, this)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
//this.layout(l, t, r, b)
}
val rect = Rect(0, 0, 0, 0)
val paint = Paint()
override fun onDraw(canvas: Canvas) {
//super.onDraw(canvas)
Log.d("##", "PuzzleView.onDraw()")
rect.right = width - 10
rect.bottom = height - 10
val bkgrColor = ContextCompat.getColor(App.context, R.color.Green)
paint.style = Paint.Style.FILL
paint.color = bkgrColor
canvas.drawRect(rect, paint)
}
}
A few thoughts on the above class/code:
override fun onLayout is required
I have other custom views that work fine (not in relation to a RecyclerView) with an empty onLayout
I tried (it's commented out in the above code) this.layout(l, t, r, b) but get a stack overflow exception
My only real thoughts here are (1) that there's something I'm supposed to be doing in this onLayout method, but I can't think of what; or (2) there's something wrong with the way that I'm inflating item_puzzle, but - again - I can't think of what. (I tried a few things on this, to no avail). I cannot think of anything else!
And here's all the other code I think could possibly be relevant:
From the Fragment containing the RecyclerView (it's what is shown in the above three images):
bunch_recycler.layoutManager = GridLayoutManager(this.context, 3)
bunch_recycler.adapter = PuzzleAdapter(bunch.puzzles)
Finally, the XML for the item itself, item_puzzle:
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:cardBackgroundColor="#color/facadeLight"
app:cardElevation="0dp"
>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#drawable/shape_puzzle"
>
<TextView
android:id="#+id/puzzleItem_text_ndx"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginEnd="16dp"
android:ellipsize="end"
android:fontFamily="#font/showg"
android:maxLines="1"
android:text="17"
android:textColor="#color/facadeDark"
android:textSize="32sp"
/>
<TextView
android:id="#+id/puzzleItem_text_details"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="#+id/puzzleItem_text_ndx"
android:layout_marginStart="8dp"
android:text="details"
android:textSize="12sp"
/>
</RelativeLayout>
</androidx.cardview.widget.CardView>
Also, if "custom view to allow onDraw" isn't the correct (or best) way of accomplishing my goal here, please let me know that as well.
Using:
Windows 10
Android Studio 3.4
Kotlin 1.3.31
Gradle 3.4.0
and the following:
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.1.0-alpha05'
implementation 'androidx.core:core-ktx:1.2.0-alpha01'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0-beta01'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0-beta01'
implementation 'androidx.navigation:navigation-fragment-ktx:2.0.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.0.0'
Afterthought
I saw (in a 4 1/2-year-old youtube video overriding getView to assign subviews in a similar-seeming situation. There is no getView method to override in today's RecyclerView. The closest thing I see is getItemViewType(position: Int): Int which doesn't seem promising either (I read up on it a bit). Just thought I'd mention this in case it triggers a thought for you (where it didn't for me).
As you can see in the picture. The title is slightly off centered because of the back button on the top-left.
How can I center the title?
I dig around the source code in CollapsingToolbarLayout and finally find a trick to center the title even when the back button is visible.
How to
1. Add this class to your code:
class CollapseLayoutCenteredTitleToolbar #JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = android.support.v7.appcompat.R.attr.toolbarStyle) : Toolbar(context, attrs, defStyleAttr) {
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
super.onLayout(changed, l, t, r, b)
(0 until childCount).forEach {
val view = getChildAt(it)
// 除了CollapsingToolbarLayout,没有谁会加一个纯的View进来
if (view.javaClass == View::class.java) {
view.left = 0
}
}
}
}
Change your layout to something like this:
<demo.CollapseLayoutCenteredTitleToolbar
android:id="#+id/main_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:popupTheme="#style/ThemeOverlay.AppCompat.Light"
/>
</android.support.design.widget.CollapsingToolbarLayout>
Enjoy