When i try to set the visibility of the group on button click,it doesn't affect the view's visibility.Using com.android.support.constraint:constraint-layout:1.1.0-beta4. I've tried setting it element-wise without problems,but no success with the group.
My MainActivity.kt
private fun toggleLoginUI(show: Boolean) {
if (show) {
group.visibility = VISIBLE
} else {
group.visibility = INVISIBLE
}
}
fun onClick(view: View) {
when (view.id) {
R.id.button -> toggleLoginUI(true)
R.id.button4 -> toggleLoginUI(false)
}
}
My activity_main.xml
<android.support.constraint.ConstraintLayout..
<TextView
android:id="#+id/textView"
... />
<TextView
android:id="#+id/textView2"
... />
<Button
android:id="#+id/button"
.../>
<Button
android:id="#+id/button4"
... />
<android.support.constraint.Group
android:id="#+id/group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="visible"
app:constraint_referenced_ids="textView,textView2" />
</android.support.constraint.ConstraintLayout>
Update: This was reported as fixed in ConstraintLayout version 2.0.0 beta 6. See bug fixes for ConstraintLayout 2.0.0 beta 6 .
This looks like a bug to me. GONE works but INVISIBLE doesn't and I think it should. It may be worth a bug report unless someone can post where my thinking is wrong. (I am using constraint-layout:1.1.0-beta4.)
In the meantime, here is a work-around that explicitly retrieves the ids within the group and sets the visibility of each retrieved view.
Within MainActivity.kt
private fun toggleLoginUI(show: Boolean) {
if (show) {
setGroupVisibility(mLayout, group, Group.VISIBLE)
} else {
setGroupVisibility(mLayout, group, Group.INVISIBLE)
}
}
private fun setGroupVisibility(layout: ConstraintLayout, group: Group, visibility: Int) {
val refIds = group.referencedIds
for (id in refIds) {
layout.findViewById<View>(id).visibility = visibility
}
}
mLayout is the ConstraintLayout.
Update: Here is another work-around that takes advantage of the fact that changing to/from GONE works as expected:
private fun toggleLoginUI(show: Boolean) {
if (show) {
group.visibility = GONE
group.visibility = VISIBLE
} else {
group.visibility = GONE
group.visibility = INVISIBLE
}
}
You can also simply call requestLayout method after changing Group visibility to View.INVISIBLE.
fun makeGroupInvisible(group: Group) {
group.visibility = View.INVISIBLE
group.requestLayout()
}
Problem is that android.support.constraint.Group updates visibility of its members in updatePreLayout method which is called from onMeasure in ConstraintLayout.
android.support.constraint.Group has a public method
public void updatePreLayout(ConstraintLayout container) {
...
}
that updates children visibilities, so calling
dataGroup.visibility = if (visible) View.VISIBLE else View.INVISIBLE
dataGroup.updatePreLayout(root)
worked for me
Had the same problem and nothing from above helps. My solution was to setVisibility(viewId, ConstraintSet.VISIBLE) inside constraint set and apply that to ConstraintLayout view.
For example:
myContstraintSet.apply {
setVisibility(firstGroup.id, ConstraintSet.VISIBLE)
setVisibility(secondGroup.id, ConstraintSet.GONE)
connect(oneView.id, ConstraintSet.BOTTOM, R.id.secondView, ConstraintSet.TOP)
clear(anotherView.id, ConstraintSet.TOP)
}
myContstraintSet.applyTo(myConstraintLayout)
just add follow line you can change it .
so it visible .
group.visibility=ConstraintLayout.GONE
Just clean your project or Rebuild your project
Related
I have a Custom View class so for the entire view I need to check the focus state to see if if is focussed or not focused.
What I have tried
used setOnFocusChangeListener { } callback to check focused state.
The problem here is that this callback is not triggered the first
time and few other times and behaviour is not consistent in trigerring the callback.
I have even set focusable=true, focusIntouchMode = true attributes to the view.
Please provide some inputs in case I am missing something here. Thanks
Here's some code
CarouselCardView constructor(context) : CustomWidget(context),ViewPager.OnPageChangeListener {
private var currentPage = 0
override fun getLayoutId(): Int {
return R.layout.banner
}
init {
isFocusable = true
isFocusableInTouchMode = true
}
private fun setViewpagerAutoAdvanceFeature(
modelData: List<ContentData>
) {
//Auto scroll logic goes here
}
override fun bindData(modelData: List<ContentData>) {
val pagerAdapter = HeroPagerAdapter(context, model = modelTV,
onClickItem = { position ->
clickListener.onItemSelected(uiComponent, uiComponent.componentItems[position])
},
onLastItemReached = { viewPager.setCurrentItem(0, true) })
viewPager.adapter = pagerAdapter
}
//Call backs don't trigger
setOnFocusChangeListener { v, hasFocus ->
if (hasFocus) {
Log.e("ANI","On setOnFocusChangeListener showing")
} else {
Log.e("ANI","On setOnFocusChangeListener not showing")
}
}
Here's the layout for reference
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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:id="#+id/heroBanner"
android:background="#drawable/bg_transparent">
<androidx.viewpager.widget.ViewPager
android:id="#+id/viewPager"
android:layout_width="#dimen/grid_900"
android:layout_height="#dimen/grid_380"
android:fitsSystemWindows="true" />
<com.google.android.material.tabs.TabLayout
android:id="#+id/indicator"
android:layout_width="#dimen/grid_900"
android:layout_height="#dimen/grid_1"
android:layout_alignBottom="#+id/viewPager"
app:tabBackground="#drawable/btn_selector"
android:layout_gravity="center_horizontal"
android:background="#android:color/transparent"
android:layout_marginBottom="#dimen/margin_bottom_line_indicator"
app:tabIndicatorHeight="0dp"
app:tabGravity="center" />
</RelativeLayout>
I made a custome view and i tried to call setOnFocusChangeListener in view class like what you provided in the question. but after calling setOnFocusChangeListener like
setOnFocusChangeListener { v, hasFocus ->
if (hasFocus) {
Log.e("ANI","On setOnFocusChangeListener showing")
} else {
Log.e("ANI","On setOnFocusChangeListener not showing")
}
}
Android studio gives an error and saying "Expecting member declaration". but I could acheive that by implementing OnFocusChangeListener like this
class MyView : View, View.OnFocusChangeListener {
...
override fun onFocusChange(v: View?, hasFocus: Boolean) {
}
}
Android 3.5
Kotlin 1.3
I have the following method that passes in a parameter that could be VISIBLE, INVISIBLE, or GONE
fun setPromotionVisibility(Int: toVisiblity) {
tvPromoation.visibility = toVisibility
}
However, when I call this method I could pass in any Int that might not be a visibility i.e.
setPromotionVisibility(234)
instead of doing this:
setPromotionVisibility(View.VISIBLE)
Just wondering if there anything I could do to force the user of the method to only enter VISIBLE, INVISIBLE, or GONE
Many thanks in advance
You can create a type-safe approach with an enum:
enum class Visibility(
val asInt: Int
) {
VISIBLE(View.VISIBLE),
INVISIBLE(View.INVISIBLE),
GONE(View.GONE),
}
which you then use as a parameter type:
fun setPromotionVisibility(toVisiblity: Visibility) {
tvPromoation.visibility = toVisibility.asInt
}
Use Annotation for this
#IntDef({View.VISIBLE, View.INVISIBLE, View.GONE})
#Retention(RetentionPolicy.SOURCE)
public #interface Visibility {}
fun setPromotionVisibility(#Visibility toVisiblity: Int) {
tvPromoation.visibility = toVisibility
}
I don't know if it is useful for your case, but in my projects, I almost never use INVISIBLE.
So, I made an extension function
fun View.visible(value: Boolean) {
visibility = if (value) View.VISIBLE else View.GONE
}
It also can be better:
fun View.visible(value: Boolean, animated: Boolean = false) {
if (animated) {
if (value) animate().alpha(1F).withStartAction { visibility = View.VISIBILE }
else animate().alpha(0F).withEndAction { visibility = View.GONE }
} else visibility = if (value) View.VISIBLE else View.GONE
}
I'm trying to write simple test for pull to refresh as a part of integration testing. I'm using the newest androidX testing components and Robolectric. I'm testing isolated fragment in which one I'm injecting mocked presenter.
XML layout part
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="#+id/refreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerTasks"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
Fragment part
binding.refreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
#Override
public void onRefresh() {
presenter.onRefresh();
}
});
Test:
onView(withId(R.id.refreshLayout)).perform(swipeDown());
verify(presenter).onRefresh();
but test doesn't pass, message:
Wanted but not invoked: presenter.onRefresh();
The app works perfectly fine and pull to refresh calls presenter.onRefresh(). I did also debugging of the test and setOnRefreshListener been called and it's not a null. If I do testing with custom matcher to check the status of SwipeRefreshLayout test passes.
onView(withId(R.id.refreshLayout)).check(matches(isRefreshing()));
I did some minor investigation over last weekend since I was facing the same issue and it was bothering me. I also did some comparing with what happens on a device to spot the differences.
Internally androidx.swiperefreshlayout.widget.SwipeRefreshLayout has an mRefreshListener that will run when onAnimationEnd is called. The AnimationEnd will trigger then OnRefreshListener.onRefresh method.
That animation listener (mRefreshListener) is passed to the mCircleView (CircleImageView) and the circle animation start is called.
On a device when the view draw method is called it will call the applyLegacyAnimation method that will, in turn, call the AnimationStart method. At the AnimationEnd, the onRefresh method will be called.
On Robolectric the draw method of the View is never called since the items are not actually drawn. This means that the animation will never run and thus neither will the onRefresh method.
My conclusion is that with the current version of Robolectric is not possible to verify that the onRefresh called due to implementation limitations. It seems though that it is planned to have a realistic rendering in the future.
I'm finally able to solve this using a hacky way :
fun swipeToRefresh(): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View>? {
return object : BaseMatcher<View>() {
override fun matches(item: Any): Boolean {
return isA(SwipeRefreshLayout::class.java).matches(item)
}
override fun describeMismatch(item: Any, mismatchDescription: Description) {
mismatchDescription.appendText(
"Expected SwipeRefreshLayout or its Descendant, but got other View"
)
}
override fun describeTo(description: Description) {
description.appendText(
"Action SwipeToRefresh to view SwipeRefreshLayout or its descendant"
)
}
}
}
override fun getDescription(): String {
return "Perform swipeToRefresh on the SwipeRefreshLayout"
}
override fun perform(uiController: UiController, view: View) {
val swipeRefreshLayout = view as SwipeRefreshLayout
swipeRefreshLayout.run {
isRefreshing = true
// set mNotify to true
val notify = SwipeRefreshLayout::class.memberProperties.find {
it.name == "mNotify"
}
notify?.isAccessible = true
if (notify is KMutableProperty<*>) {
notify.setter.call(this, true)
}
// mockk mRefreshListener onAnimationEnd
val refreshListener = SwipeRefreshLayout::class.memberProperties.find {
it.name == "mRefreshListener"
}
refreshListener?.isAccessible = true
val animatorListener = refreshListener?.get(this) as Animation.AnimationListener
animatorListener.onAnimationEnd(mockk())
}
}
}
}
I am struggling to find the specific action which is blocking my UI thread, i've tried several Scheduler operators, but I'm not sure how to make it work.
I have a UI with button, which onClicked is starting bluetooth scan and updates the textView with strings like a log(it shows what is happening at the moment).
So here is my MainActivity:
lateinit var disposable: Disposable
val textDataService = TextDataService()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_scan_test)
buttonScanTestStart.setOnClickListener {
if (isBluetoothEnabled()) {
textViewLog.text = ""
buttonScanTestStop.visibility = View.VISIBLE
buttonExportScanTestRaportSummary.visibility = View.GONE
buttonExportScanTestRaportFull.visibility = View.GONE
buttonScanTestStart.visibility = View.GONE
disposable=
Scanner()
.discoverSingleDevice(this, " ", textViewLog)
.doOnError {
setText("General error: ${it.message ?: it::class.java}", textViewLog)
setLogText("General error: ${it.message ?: it::class.java}")
}
.repeat(1)
.doOnComplete {
buttonScanTestStop.visibility = View.GONE
}
.doOnDispose {
log("TEST DISPOSED")
}
.subscribe()
} else {
Toast.makeText(this, "Please, enable bluetooth to start test.", Toast.LENGTH_SHORT).show()
}
}
buttonScanTestStop.setOnClickListener {
disposable.dispose()
buttonScanTestStop.visibility = View.GONE
buttonScanTestStart.visibility = View.VISIBLE
buttonExportScanTestRaportSummary.visibility = View.VISIBLE
buttonExportScanTestRaportFull.visibility = View.VISIBLE
textDataService.generateScanGeneralStatisticsLogText(textViewLog)
}
Here is the Scanner class:
class Scanner {
private fun scan(context: Context, textView: TextView) = Observable.create<ScannedItem> { emitter ->
val bluetoothAdapter = context.getBluetoothAdapter()
if (bluetoothAdapter.isEnabled) {
val scanCallback = BluetoothAdapter.LeScanCallback { bluetoothDevice, rssi, _ ->
if(bluetoothDevice.name == null){
bluetoothRawLog("Scanned Item -> ${bluetoothDevice.logText()} | rssi = $rssi | time: = ${getCurrentTime()}")
scannedOtherDevices.add(bluetoothDevice.address)
}
else{
if(!scannedDevices.contains(bluetoothDevice.name)){
setText("Scanned Item -> ${bluetoothDevice.logText()} | rssi = $rssi | time: = ${getCurrentTime()}\n", textView)
setLogText("Scanned Item -> ${bluetoothDevice.logText()} | rssi = $rssi | time: = ${getCurrentTime()}\n")
}
scannedDevices.add(bluetoothDevice.name)
}
bluetoothRawLog("Scanned Item -> ${bluetoothDevice.logText()} | rssi = $rssi")
bluetoothDevice.name?.let {
emitter.onNext(ScannedItem(it, bluetoothDevice.address))
}
}
emitter.setCancellable { bluetoothAdapter.stopLeScan(scanCallback) }
bluetoothAdapter.startLeScan(scanCallback)
} else
emitter.onError(IllegalStateException("Bluetooth turned off"))
}
.doOnNext { log("Scanned -> $it") }
.timeout(12, TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread())
.doOnError { if (it is TimeoutException) setLogText("Scanner -> no named devices for 12 seconds: resetting...") }
.retry { t -> t is TimeoutException }
fun discoverSingleDevice(context: Context, searchName: String, textView: TextView): Observable<ScannedItem> = scan(context, textView)
.filter { it.name.contains(searchName) }
.take(1)
And here is my Kotlin extension functions, which i am using to set Text:
fun setLogText(text: String) {
logBuffer.append(text)
}
fun setText(text: String, textView: TextView) {
textView.append(text)
}
Here is also my layout:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:layout_margin="16dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="horizontal">
<Button
android:layout_weight="1"
android:id="#+id/buttonScanTestStart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Start Test" />
<Button
android:layout_weight="1"
android:visibility="gone"
android:id="#+id/buttonScanTestStop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Stop Test" />
<Button
android:layout_weight="1"
android:id="#+id/buttonExportScanTestRaportFull"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Export raport"
android:visibility="gone" />
<Button
android:layout_weight="1"
android:id="#+id/buttonExportScanTestRaportSummary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Export summary"
android:visibility="gone" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:visibility="gone"
android:id="#+id/scanTestStartedLinearLayout"
android:gravity="center"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:text="Test running..."
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ProgressBar
android:id="#+id/scanTestProgressBar"
android:layout_width="16dp"
android:layout_height="16dp" />
</LinearLayout>
<TextView
android:id="#+id/textViewScanTestLog"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:gravity="center" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:gravity="center">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="#+id/textViewLog"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</ScrollView>
</LinearLayout>
So the text is appearing in my textView pretty smooth, everything is OK.
However, when there is more amount of available devices to scan i can't stop the test by clicking the button. It is just freezed and responding like way too late when i try to click it.
The discoverSingleDevice function is will be run forever, because i filter to have the search name from my editText(which i hardcoded with " ") to make it run forever and i want to only stop this by clicking the stop button in my layout. As a second parameter i passed to it textViewLog from my layout(Kotlin is smart enough to find it by id)
So how to prevent blocking the UI with this setup?
UPDATE:
As WenChao suggested, I've done something like this:
val observable = Scanner()
.discoverSingleDevice(this, " ", textViewLog)
.doOnError {
nexoSetText("General error: ${it.message ?: it::class.java}", textViewLog)
nexoSetLogText("General error: ${it.message ?: it::class.java}")
}
.repeat(1)
.doOnComplete {
buttonScanTestStop.visibility = View.GONE
}
.doOnDispose {
nexoLog("TEST DISPOSED")
}
disposable = observable
.subscribeOn(Schedulers.newThread()).subscribe()
However, it not helped, it is not that simple. Again, I've tried several things with other operators.
Please, be specific to my case - I provided code.
It is like after 2 minutes of scanning the app is rather freezing. In the logs of course appearing I/Choreographer: Skipped 54 frames! The application may be doing too much work on its main thread.
UPDATE 2:
As PhoenixWang suggested, I've log to check on which thread the BluetoothAdapter runs. So I've done it like this in my scan method in the Scanner class:
private fun scan(context: Context, textView: TextView) = Observable.create<ScannedItem> { emitter ->
val bluetoothAdapter = context.getBluetoothAdapter()
if (bluetoothAdapter.isEnabled) {
val scanCallback = BluetoothAdapter.LeScanCallback { bluetoothDevice, rssi, _ ->
Log.v("BluetoothThread", "Running on: " + Thread.currentThread().name)
///the rest of the code
So how to already fix this? I've thought that it will actually run on the separate thread(not main) because i've done the naive fix, which is visible in my first Update. Can you help?
UPDATE 3:
So the problem is with the BluetoothAdapter.leScan - it is running on main thread, even if i do something like this:
Observable.fromCallable{ bluetoothAdapter.startLeScan(scanCallback)}.subscribeOn(Schedulers.newThread()).subscribe()
As PhoenixWang suggesting, BluetoothAdapter.LeScanCallback run in mainthread because of his implementation. Observable/RxJava can't change it.
So how to resolve my problem?
RxJava is synchronous by default. In your case, it runs on the caller thread which is the main thread of Android
You have to specify what thread you want the subscription to happen.
Observable.fromCallable(whatever())
.subscribeOn(Schedulers.newThread()).subscribe()
read more here.
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
Add .subscribeOn(Schedulers.io()) along with .observeOn(AndroidSchedulers.mainThread()) like above. Try this. Should work.
To be more clear. I make a answer but can not solve your problem .
Your code:
Observable.fromCallable{ bluetoothAdapter.startLeScan(scanCallback)}.subscribeOn(Schedulers.newThread()).subscribe().
means that your bluetoothAdapter.startLeScan(scanCallback) is run in the newThread. But not how bluetoothAdapter call you back.
So in the implementation of your startLeScan. It can start a new thread or run in other thread depends on it's implementation.
Same idea with your Observable.create(//your stuff)
They are guaranteed running on your specified scheduler. But it can also start a new thread inside of it. That's why I mentioned in comment to check the implementation of your BluetoothAdapter.
Update
To be more clear, an example implementation of BluetoothAdapter that can block your UI Thread.
class BluetoothAdapter {
fun startLeScan(callback: LeScanCallback) {
val handler = Handler(Looper.getMainLooper())
handler.post {
// here you back to your UI Thread.
callback.onSuccess(1)
}
}
interface LeScanCallback {
fun onSuccess(result: Int)
}
}
So if your implementation of BluetoothAdapter will change the thread back to UI thread. You eventually will block your UI thread no matter what thread of your Observable. That what I mean Check the implementation of your BluetoothAdapter
Does the extensions have a magic to call inflated view? As far as I see, I should break the harmony of code and call findViewById.
The intent was to inflate layout_ongoingView layout at sometime, and make hidden, and visible again based on scenario.
<ViewStub
android:id="#+id/viewStubOngoing"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inflatedId="#+id/ongoingView"
android:layout="#layout/layout_ongoingView" />
And codes
override fun hideOngoingPanel() {
if (viewStubOngoing !is ViewStub) {
findViewById(R.id.ongoingView).visibility = View.GONE
}
}
override fun showOngoingPanel() {
if (viewStubOngoing !is ViewStub) {
findViewById(R.id.ongoingView).visibility = View.VISIBLE
} else {
viewStubOngoing.inflate()
}
}