Can't change variable in Event - android

Try to detect swipe gesture and use code from android docs with liitle changes.
I try to show toast with Y-axis data. I was declared variable in the beginning of MyGestureListener class and try to change it, when onFling method called. I want to show toast with Y-axis data, but always see default string "Def_Nothing". If I add Log.d — I watch, that onFling method working and I get correct data of X and Y axis.
I think don't understood some fundamental of object oriented programming and neen little explanation of it.
class MainActivity : AppCompatActivity() {
private lateinit var mDetector: GestureDetectorCompat
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mDetector = GestureDetectorCompat(this, MyGestureListener())
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
this.mDetector.onTouchEvent(event)
val myGestureListener = MyGestureListener()
Toast.makeText(this, myGestureListener.currentGesture, Toast.LENGTH_LONG).show()
return super.onTouchEvent(event)
}
class MyGestureListener: GestureDetector.SimpleOnGestureListener() {
var currentGesture: String = "Def_Nothing"
override fun onFling(e1: MotionEvent?, e2: MotionEvent?, velocityX: Float, velocityY: Float): Boolean {
currentGesture = (e2!!.y - e1!!.y).toString()
return true
}
}
}

You have to keep a reference to your gesture listener, you cant just create a new one every time. See the example code below.
class MainActivity : AppCompatActivity() {
private lateinit var mDetector: GestureDetectorCompat
private lateinit var mGestureListener: MyGestureListener
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mGestureListener = MyGestureListener()
mDetector = GestureDetectorCompat(this, mGestureListener)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
this.mDetector.onTouchEvent(event)
Toast.makeText(this, mGestureListener.currentGesture, Toast.LENGTH_LONG).show()
return super.onTouchEvent(event)
}
class MyGestureListener: GestureDetector.SimpleOnGestureListener() {
...
}
}

Related

RecyclerView.Adapter object does not work properly after extending a class

I want this search class to search for users in a database, i put some functions i use most of the time into an abstract class called for instance "Abs" that extends "AppCompactActivity"
I changed nothing other than the extended class, it works perfectly with the old class, however when i change it to the updated one it gives an error
This is the updated class
class SearchActivity : AbsBottom(R.layout.activity_search, R.id.bottomNav, R.id.search_nav) {
private var user = mutableListOf(UserSearch())
private var userAdapter = UserViewHolder()
lateinit var binding: ActivitySearchBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySearchBinding.inflate(layoutInflater)
//setContentView(binding.root)
//layout
binding.recyclerProfile.setHasFixedSize(true)
val llm = LinearLayoutManager(this)
llm.orientation = LinearLayoutManager.VERTICAL
binding.recyclerProfile.layoutManager = llm
//region Adapter Setup
binding.recyclerProfile.adapter = userAdapter
binding.searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
//do stuff
}
override fun onQueryTextChange(newText: String?): Boolean {
//do other stuff
}
})
//endregion
}
}
This is the original
class SearchActivity : AppCompatActivity() {
private var user = mutableListOf(UserSearch())
private var userAdapter = UserViewHolder()
lateinit var binding: ActivitySearchBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySearchBinding.inflate(layoutInflater)
setContentView(binding.root)
bottomNavigation()
//layout
binding.recyclerProfile.setHasFixedSize(true)
val llm = LinearLayoutManager(this)
llm.orientation = LinearLayoutManager.VERTICAL
binding.recyclerProfile.layoutManager = llm
// userAdapter.setUsers(user)
//region Adapter Setup
binding.recyclerProfile.adapter = userAdapter
binding.searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
//do stuff
}
override fun onQueryTextChange(newText: String?): Boolean {
//do other stuff
}
})
//endregion
}
}
This is what the abstract class looks like
abstract class AbsBottom(val idC: Int, val idB: Int, val ac: Int) : AppCompatActivity() {
protected lateinit var bottomNav : BottomNavigationView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(idC)
bottomNav = findViewById(idB)
bottomNav.selectedItemId = ac
//etc
}
}
The error doesn't say much, it says that it can't see the userAdapter, and as a result it doesn't load the list of searched users
Fixed the issue, commenting the line
//setContentView(binding.root)
broke the binding.

How to add GestureDetector and ScaleGestureDetector on a ImageView with setOnTouchListener

In my MainActivity i have:
class MainActivity : AppCompatActivity() {
lateinit var imageView: ImageView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val imageViewGestureDetector = GestureDetector( this, object : GestureDetector.SimpleOnGestureListener() {
override fun onDoubleTap(e: MotionEvent?): Boolean {
return true
}
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean {
Log.i("MainActivity", "Fling!")
return super.onFling(e1, e2, velocityX, velocityY)
}
})
imageView.setOnTouchListener { _, event -> imageViewGestureDetector.onTouchEvent(event) }
}
}
Now I thought it might be nice to define a behaviour for onScale but I noticed Scale Gestures are detected by the ScaleGestureDetector.
I'd rather not sub-class the ImageView and override fun onTouchEvent() - or should I? That's what all of the solutions in the net seem to suggest. I'd assume there might be a convenient way to combine these common gestures in one view.
I found a quite simple way myself.
I wrote a ScaleGestureDetector like that
val imageViewScaleGestureDetector = ScaleGestureDetector(this, object: ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector?): Boolean {
Log.i(“MainActivity”, “scale”)
return super.onScale(detector)
}
})
and changed the line
imageView.setOnTouchListener { _, event -> imageViewGestureDetector.onTouchEvent(event) }
to
imageView.setOnTouchListener { _, event ->
imageViewScaleGestureDetector.onTouchEvent(event)
imageViewGestureDetector.onTouchEvent(event)
}
My ImageView is detecting both fling and scale gesture. Let me know if that's bad practice or there are better solutions.

How to dismiss Bottom Sheet fragment when click outside in Kotlin?

I make bottom sheet fragment like this:
val bottomSheet = PictureBottomSheetFragment(fragment)
bottomSheet.isCancelable = true
bottomSheet.setListener(pictureListener)
bottomSheet.show(ac.supportFragmentManager, "PictureBottomSheetFragment")
But its not dismiss when I touch outside. and dismiss or isCancelable not working.
try this
behavior.setState(BottomSheetBehavior.STATE_HIDDEN));
You can override method and indicate, for example, in onViewCreated what you need:
class ModalDialogSuccsesDataPatient : ModalDialog() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
isCancelable = false //or true
}
}
Let's try to design reusable functions to solve this problem and similar ones if the need arises.
We can create extension functions on View that tell whether a point on the screen is contained within the View or not.
fun View.containsPoint(rawX: Int, rawY: Int): Boolean {
val rect = Rect()
this.getGlobalVisibleRect(rect)
return rect.contains(rawX, rawY)
}
fun View.doesNotContainPoint(rawX: Int, rawY: Int) = !containsPoint(rawX, rawY)
Now we can override the dispatchTouchEvent(event: MotionEvent) method of Activity to know where exactly the user clicked on the screen.
private const val SCROLL_THRESHOLD = 10F // To filter out scroll gestures from clicks
private var downX = 0F
private var downY = 0F
private var isClick = false
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
when (event.action and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_DOWN -> {
downX = event.x
downY = event.y
isClick = true
}
MotionEvent.ACTION_MOVE -> {
val xThreshCrossed = abs(downX - event.x) > SCROLL_THRESHOLD
val yThreshCrossed = abs(downY - event.y) > SCROLL_THRESHOLD
if (isClick and (xThreshCrossed or yThreshCrossed)) {
isClick = false
}
}
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
if (isClick) onScreenClick(event.rawX, event.rawY)
}
else -> { }
}
return super.dispatchTouchEvent(event)
}
private fun onScreenClick(rawX: Float, rawY: Float) { }
Now, you can simply use the above-defined functions to achieve the required result
private fun onScreenClick(rawX: Float, rawY: Float) {
if (bottomSheet.doesNotContainPoint(rawX.toInt(), rawY.toInt())) {
// Handle bottomSheet state changes
}
}
What more? If you have a BaseActivity which is extended by all your Activities then you can add the click detection code to it. You can make the onScreenClick an protected open method so that it can be overridden by the sub-classes.
protected open fun onScreenClick(rawX: Float, rawY: Float) { }
Usage:
override fun onScreenClick(rawX: Float, rawY: Float) {
super.onScreenClick(rawX, rawY)
if (bottomSheet.doesNotContainPoint(rawX.toInt(), rawY.toInt())) {
// Handle bottomSheet state changes
}
}

Where to poll sensor data in ViewModel environment?

I like to get sensor data (eg. gyroscope) from a fragment using mvvm. So far I got it working but only in a fragment, completely working around the mvvm environment. It doesn't work from the viewmodel file. How can I stream sensor data into LiveData?
This is working code, but bypassing viewmodel:
class RPMFragment : Fragment(), SensorEventListener {
private lateinit var rpmViewModel: RPMViewModel
private lateinit var sensorManager: SensorManager
private lateinit var gyroscope: Sensor
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
rpmViewModel = ViewModelProviders.of(this).get(RPMViewModel::class.java)
val root = inflater.inflate(R.layout.fragment_rpm, container, false)
val textView: TextView = text_rpm
rpmViewModel.text.observe(this, Observer {
textView.text = it
})
this.sensorManager = activity!!.getSystemService(Context.SENSOR_SERVICE) as SensorManager
sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)?.let {
this.gyroscope = it
}
return root
}
override fun onSensorChanged(event: SensorEvent?) {
if (event?.sensor?.type == Sensor.TYPE_GYROSCOPE) {
text_rpm.text = "z axis: " + event.values[2].toString()
}
}
override fun onResume() {
super.onResume();
sensorManager.registerListener(this, this.gyroscope, SensorManager.SENSOR_DELAY_NORMAL);
// repeat that line for each sensor you want to monitor
}
override fun onPause() {
super.onPause();
sensorManager.unregisterListener(this);
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
}
You can write custom LiveData class to do the "heavy lifting" and let your ViewModel just be a simple host:
// viewmodel only serves as a host for livedata
class RPMViewModel(application: Application) : AndroidViewModel(application) {
val rpmLiveData = RPMLiveData()
// inner class just to have access to application
inner class RPMLiveData : LiveData<String>(), SensorEventListener {
private val sensorManager
get() = getApplication<Application>().getSystemService(Context.SENSOR_SERVICE) as SensorManager
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
//unused
}
override fun onSensorChanged(event: SensorEvent) {
postValue("z axis: ${event.values[2]}")
}
override fun onActive() {
sensorManager.let { sm ->
sm.getDefaultSensor(Sensor.TYPE_GYROSCOPE).let {
sm.registerListener(this, it, SensorManager.SENSOR_DELAY_NORMAL)
}
}
}
override fun onInactive() {
sensorManager.unregisterListener(this)
}
}
}
With this ViewModel in place your fragment is now lightweight and only reacts to changes from LiveData:
class RPMFragment : Fragment() {
private val rpmViewModel by lazy {
ViewModelProviders.of(this).get(RPMViewModel::class.java)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val root = inflater.inflate(R.layout.fragment_rpm, container, false)
rpmViewModel.rpmLiveData.observe(viewLifecycleOwner, Observer {
text_rpm.text = it
})
return root
}
}
You have to make your ViewModel extend AndroidViewModel to get access to the current Context. Then let your ViewModel implement the SensorEventListener Interface to get informed about sensor events. Post the event data to your LiveData:
class RpmViewModel(application: Application) : AndroidViewModel(application), SensorEventListener {
private lateinit var sensorManager: SensorManager
private lateinit var gyroscope: Sensor
val sensorDataLiveData = MutableLiveData<String>()
fun registerSensors() {
sensorManager = getApplication<Hero2Application>().getSystemService(Context.SENSOR_SERVICE) as SensorManager
sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)?.let {
this.gyroscope = it
}
sensorManager.registerListener(this, this.gyroscope, SensorManager.SENSOR_DELAY_NORMAL);
}
fun unregisterSensors() {
sensorManager.unregisterListener(this)
}
override fun onSensorChanged(event: SensorEvent?) {
if (event?.sensor?.type == Sensor.TYPE_GYROSCOPE) {
sensorDataLiveData.postValue("z axis: " + event.values[2].toString())
}
}
override fun onAccuracyChanged(p0: Sensor?, p1: Int) {
}
}
In your Fragment you could then listen to the Event like this:
viewModel.sensorDataLiveData.observe(this, Observer {
Log.d("RPMFragment", it)
})
Register and unregister like you did before:
override fun onResume() {
super.onResume()
viewModel.registerSensors()
}
override fun onPause() {
super.onPause()
viewModel.unregisterSensors()
}

Combining ViewModels With ViewPagers

I am making a simple App which holds a list of timers and allows the user to scroll between these timers via ViewPager. Android Architecture Component principles are in effect here: the ViewModel holds the state of the app, and Fragments/Activities just handle UI interactions.
In this case, a custom ViewPager and FragmentStatePagerAdapter has been made. My question is, what is the best way to have the ViewPager/adapter respond to changes in the ViewModel?
The current code below works fine, but is there a better way to do this? Currently, both the ViewPager and Adapter require reference to the ViewModel and this feels contrary to what MVVM should implement.
ViewPager
class VerticalTimerViewPager(context: Context, attributeSet: AttributeSet): ViewPager(context, attributeSet){
var homeViewModel: HomeViewModel?=null
init {
setPageTransformer(true, VerticalPageTransformer())
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
ev ?: return false
val intercepted = super.onInterceptTouchEvent(swapXYOnMotionEvent(ev))
swapXYOnMotionEvent(ev)
return intercepted
}
override fun onTouchEvent(ev: MotionEvent?): Boolean {
homeViewModel ?: throwNoViewModelException()
when(ev?.action){
MotionEvent.ACTION_DOWN -> homeViewModel?.onSwipeDown()
MotionEvent.ACTION_UP -> homeViewModel?.onSwipeUp()
}
return super.onTouchEvent(swapXYOnMotionEvent(ev ?: return false))
}
private fun swapXYOnMotionEvent(motionEvent: MotionEvent): MotionEvent{
with(motionEvent){
val newX = (y/height)*width
val newY = (x/width) * height
setLocation(newX, newY)
}
return motionEvent
}
private fun throwNoViewModelException():Boolean{
throw RuntimeException("${this.javaClass.simpleName}: HomeViewModel must be set by setting the variable homeViewModel")
}
}
FragmentStatePagerAdapter
class VerticalTimerViewPager(context: Context, attributeSet: AttributeSet): ViewPager(context, attributeSet){
var homeViewModel: HomeViewModel?=null
init {
setPageTransformer(true, VerticalPageTransformer())
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
ev ?: return false
val intercepted = super.onInterceptTouchEvent(swapXYOnMotionEvent(ev))
swapXYOnMotionEvent(ev)
return intercepted
}
override fun onTouchEvent(ev: MotionEvent?): Boolean {
homeViewModel ?: throwNoViewModelException()
when(ev?.action){
MotionEvent.ACTION_DOWN -> homeViewModel?.onSwipeDown()
MotionEvent.ACTION_UP -> homeViewModel?.onSwipeUp()
}
return super.onTouchEvent(swapXYOnMotionEvent(ev ?: return false))
}
private fun swapXYOnMotionEvent(motionEvent: MotionEvent): MotionEvent{
with(motionEvent){
val newX = (y/height)*width
val newY = (x/width) * height
setLocation(newX, newY)
}
return motionEvent
}
private fun throwNoViewModelException():Boolean{
throw RuntimeException("${this.javaClass.simpleName}: HomeViewModel must be set by setting the variable homeViewModel")
}
}
Instantiation Method called in Activity onCreate()
private fun setupPagerAdapter() {
val pagerAdapter = TimerSlidePagerAdapter(viewModel = viewModel, fragmentManager = supportFragmentManager)
with(pager){
adapter = pagerAdapter
homeViewModel = viewModel
}
}
Looks like you're using ViewModel to communicate for onTouchEvent(ev: MotionEvent?) from your adapter & view pager, if you want to make your adapter & view pager independent form ViewModel i would suggest you following way:
Make one interface having methods onSwipeDown() & onSwipeUp() (or make any N numbers of method you want to have for ViewModel)
Take this interface object instead of ViewModel in your view pager & adapter.
Implement this interface to your ViewModel (which will expose methods of interface in ViewModel).
Cast your interface to ViewModel where you're passing ViewModel to adapter and view pager.
Voila! you've made your adapter & view pager independent to ViewModel's direct reference.
The Answer from Jeel Vankhede was marked as a solution and implemented. For future reference, the solution code will be pasted below to show the specifics of how the ViewPager and Adapter were successfully made independent of the ViewModel:
ViewPager:
notice we now have an Interface object. When the ViewPager is swiped, if there is no interface object, an exception is thrown.
//Overriding default touch events and swapping x/y coordinates prior to handling
class VerticalTimerViewPager(context: Context, attributeSet: AttributeSet): ViewPager(context, attributeSet){
var timerViewPagerEventListener: TimerViewPagerEvent? = null
init {
setPageTransformer(true, VerticalPageTransformer())
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
ev ?: return false
val intercepted = super.onInterceptTouchEvent(swapXYOnMotionEvent(ev))
swapXYOnMotionEvent(ev)
return intercepted
}
override fun onTouchEvent(ev: MotionEvent?): Boolean {
timerViewPagerEventListener ?: throwInterfaceExeption()
when(ev?.action){
MotionEvent.ACTION_DOWN -> timerViewPagerEventListener?.onViewPagerSwipeDown()
MotionEvent.ACTION_UP -> timerViewPagerEventListener?.onViewPagerSwipeUp()
}
return super.onTouchEvent(swapXYOnMotionEvent(ev ?: return false))
}
private fun throwInterfaceExeption():Boolean {
throw RuntimeException("${this.javaClass.simpleName}: Parent Activity must implement TimerViewPagerEvent interface" )
}
private fun swapXYOnMotionEvent(motionEvent: MotionEvent): MotionEvent{
with(motionEvent){
val newX = (y/height)*width
val newY = (x/width) * height
setLocation(newX, newY)
}
return motionEvent
}
interface TimerViewPagerEvent{
fun onViewPagerSwipeUp()
fun onViewPagerSwipeDown()
}
}
Adapter: Now takes a list of objects (Timers in this case)
class TimerSlidePagerAdapter(fragmentManager: FragmentManager, private val timers: List<TimerEntity>?):
FragmentStatePagerAdapter(fragmentManager){
override fun getItem(position: Int): Fragment {
if (count > 0) {
return makeTimerFragment(position)
}
return NoDataFragment()
}
override fun getCount(): Int = timers?.size ?: 0
//Internal Functions
private fun makeTimerFragment(position: Int): Fragment {
timers ?: return NoDataFragment()
val timeInMS = timers[position].timeInMS
return if (timeInMS > 0) {
TimerFragment.newInstance(timeInMS)
} else {
NoDataFragment()
}
}
}
Activity Implementation:
private fun setupPagerAdapter() {
val pagerAdapter = TimerSlidePagerAdapter(
fragmentManager = supportFragmentManager,
timers = viewModel?.timers?.value)
with(pager){
adapter = pagerAdapter
timerViewPagerEventListener = this#MainActivity
}
}
private fun observeTimers(){
viewModel.timers.observe(this, Observer {timerList->
timerList ?: Log.e(this.javaClass.simpleName, "List of timers is null")
.also {return#Observer}
setupPagerAdapter()
})
}

Categories

Resources