I am making an android app, it has an editText on the top and a recyclerview on the buttom.
Every time when a user hit "enter" in the editText field, I will then make an API call with OKHttp and save the info I needed to a Singleton Class so my adapter can use it and update the list.
However, the recyclerview is not showing(updating) if the focus is not in the editText
"V/ViewRootImpl: The specified message queue synchronization barrier token has not been posted or has already been removed"
It's showing this everytime when I click on the editText, I googled it and found out it might be the thread issues. The only place I interact with thread is the API request I made with okHTTP. However, the request returns and interpret the json file correctly.
public Boolean makeCall (String parameter) throws IOException, UnirestException {
// make the api call
String completedLink = BASE_LINK + parameter;
Future<HttpResponse<com.mashape.unirest.http.JsonNode>> response = Unirest.get("https://mashape-community-urban-dictionary.p.rapidapi.com/define?term=wat")
.header("x-rapidapi-host", "mashape-community-urban-dictionary.p.rapidapi.com")
.header("x-rapidapi-key", "xxxxx")
.asJsonAsync(new Callback<com.mashape.unirest.http.JsonNode>() {
#Override
public void completed(HttpResponse<com.mashape.unirest.http.JsonNode> response) {
// decode the response body
ObjectMapper objectMapper = new ObjectMapper();
JsonNode node = null;
try {
node = objectMapper.readTree(String.valueOf(response.getBody()));
} catch (JsonProcessingException ex) {
ex.printStackTrace();
}
JsonNode resultNode = node.get("list");
List<WordItem> resultList = null;
try {
resultList = objectMapper.readValue(resultNode.toString(),
new TypeReference<List<WordItem>>() {});
} catch (JsonProcessingException ex) {
ex.printStackTrace();
}
SavedInfo.getInstance().setResult(resultList);
}
#Override
public void failed(UnirestException e) {
}
#Override
public void cancelled() {
}
});
return true;
}
Every time when I hit enter on the soft keyboard, my code will start making the API call
mResult.setAdapter(adapter);
mResult.setNestedScrollingEnabled(false);
mResult.addItemDecoration(new DividerItemDecoration(getContext(), DividerItemDecoration.VERTICAL));
mResult.setLayoutManager(new LinearLayoutManager(getContext()));
mSearchBox.setOnEditorActionListener(new TextView.OnEditorActionListener() {
#Override
public boolean onEditorAction(TextView textView, int i, KeyEvent keyEvent) {
if (i == EditorInfo.IME_ACTION_DONE) {
// show progress bar
hideProgressBar(false, mProgress);
mHomeText.setText(textView.getText());
try {
if (new HttpRequest().makeCall(textView.getText().toString())){
adapter.updateList();
}
else {
// showing error message
}
} catch (IOException e) {
e.printStackTrace();
return false;
}
hideProgressBar(true, mProgress);
// hide keyword when enter key is detected
View view = getActivity().getCurrentFocus();
if (view != null) {
InputMethodManager imm = (InputMethodManager)getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
}
return true;
}
});
Also, one thing to point out is the Progress Bar(I set it to "VISIBLE" but not showing still)is not showing at all. I set a few places throughout my code, the Singleton Class and Http Requests are correct and return + save correct info.
Here is my updateList() in my adapter class just in case this helps
public void updateList () {
list.clear();
list.addAll(SavedInfo.getInstance().getResult());
notifyDataSetChanged();
}
Here is the my layout file
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.home.HomeFragment"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/constraintLayout"
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="#F3F3F3"
app:layout_constraintStart_toStartOf="parent">
<TextView
android:id="#+id/title_word_home"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="36dp"
android:fontFamily="sans-serif"
android:text="#string/home_text"
android:textAlignment="center"
android:textSize="28sp"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent"
tools:layout_editor_absoluteX="0dp" />
<EditText
android:id="#+id/searchBox"
android:layout_width="254dp"
android:layout_height="50dp"
android:layout_marginStart="239dp"
android:layout_marginEnd="239dp"
android:autofillHints=""
android:drawableStart="#drawable/ic_search"
android:drawablePadding="10dp"
android:ems="10"
android:hint="#string/searchBox_hint"
android:imeOptions="actionDone"
android:inputType="text"
android:pointerIcon="none"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/title_word_home" />
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ProgressBar
android:id="#+id/loading_progress"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="center|center_vertical"
android:indeterminate="true"
android:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/home_resultList"
android:layout_width="409dp"
android:layout_height="wrap_content"
android:visibility="visible" />
</LinearLayout>
</LinearLayout>
Thank you for reading!
The problem occurs because the data posts to recycler view before that has downloaded. You are trying to update the recycler view in the method updateList() which called after the request started but before it completed. That's also why the progress bar doesn't show - it's immediately gone.
In order to fix the problem, you have to trigger updateList() after the data has been downloaded. The good way will be to rewrite the makeCall() method, which accepts the second argument as a callback, so you can update the list.
Define the custom callback:
interface MyCallback {
void completed(List<WordItem> result);
void error(UnirestException error);
}
Rewrite making call a bit:
public void makeCall(String parameter, MyCallback callback) {
// make the api call
String completedLink = BASE_LINK + parameter;
Future<HttpResponse<com.mashape.unirest.http.JsonNode>> response = Unirest.get("https://mashape-community-urban-dictionary.p.rapidapi.com/define?term=wat")
.header("x-rapidapi-host", "mashape-community-urban-dictionary.p.rapidapi.com")
.header("x-rapidapi-key", "xxxxx")
.asJsonAsync(new Callback<com.mashape.unirest.http.JsonNode>() {
#Override
public void completed(HttpResponse<com.mashape.unirest.http.JsonNode> response) {
// decode the response body
ObjectMapper objectMapper = new ObjectMapper();
JsonNode node = null;
try {
node = objectMapper.readTree(String.valueOf(response.getBody()));
} catch (JsonProcessingException ex) {
ex.printStackTrace();
}
JsonNode resultNode = node.get("list");
List<WordItem> resultList = null;
try {
resultList = objectMapper.readValue(resultNode.toString(),
new TypeReference<List<WordItem>>() {});
} catch (JsonProcessingException ex) {
ex.printStackTrace();
}
// SavedInfo.getInstance().setResult(resultList);
callback.completed(resultList);
}
#Override
public void failed(UnirestException e) {
callback.failed(e);
}
#Override
public void cancelled() {
}
});
}
And finally pass the callback to makeCall() and refresh the list:
#Override
public boolean onEditorAction(TextView textView, int i, KeyEvent keyEvent) {
if (i == EditorInfo.IME_ACTION_DONE) {
// show progress bar
hideProgressBar(false, mProgress);
// clear your list before call if you wish
// updateList(null);
mHomeText.setText(textView.getText());
new HttpRequest().makeCall(textView.getText().toString(), new MyCallback() {
#Override
public void completed(List<WordItem> result) {
updateList(result);
hideProgressBar(true, mProgress);
}
#Override
public void error(UnirestException error) {
hideProgressBar(true, mProgress);
// showing error message
}
});
// hide keyword when enter key is detected
View view = getActivity().getCurrentFocus();
if (view != null) {
InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
}
return true;
}
This method should be rewrited as well:
public void updateList(List<WordItem> result) {
list.clear();
if (result != null) list.addAll(result);
notifyDataSetChanged();
}
After that problem should go away and also you have no longer need in the singleton class that holding your data.
By the way, there is the Retrofit library which used in order to make api requests in an easier way. The retrofit is the best practice in the android community, so I suggest to try use it.
Related
I'm writing a celsius-farenheits converter but the program crashes for something that I didn't found
I'm actually trying to use the data binding and the view model but Android Studio founded some issues in ActivityMainBindingImpl.java that I didn't write by myself. Here's the part of code were it founds a problem. It is in line 104 at com.example.convertitorecelsius_farenheit.MainViewModel viewModel = mViewModel; It says "Cannot resolve symbol 'mViewModel'"
There's another problem in line 33 in "super(bindingComponent, root, 0", it says "'ActivityMainBinding()' has private access in 'com.example.convertitorecelsius_farenheit.databinding.ActivityMainBinding'"
The last problem is at line 8 in "public class ActivityMainBindingImpl extends ActivityMainBinding {", the error is in "ActivityMainBinding", it says "Cannot inherit from final 'com.example.convertitorecelsius_farenheit.databinding.ActivityMainBinding'"
Here's the full code where I founded these problems
package com.example.convertitorecelsius_farenheit.databinding;
import com.example.convertitorecelsius_farenheit.R;
import com.example.convertitorecelsius_farenheit.BR;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.View;
#SuppressWarnings("unchecked")
public class ActivityMainBindingImpl extends ActivityMainBinding {
#Nullable
private static final androidx.databinding.ViewDataBinding.IncludedLayouts sIncludes;
#Nullable
private static final android.util.SparseIntArray sViewsWithIds;
static {
sIncludes = null;
sViewsWithIds = new android.util.SparseIntArray();
sViewsWithIds.put(R.id.cambiaTemperatura, 3);
sViewsWithIds.put(R.id.inputTemperatura, 4);
sViewsWithIds.put(R.id.converti, 5);
}
// views
#NonNull
private final androidx.constraintlayout.widget.ConstraintLayout mboundView0;
// variables
// values
// listeners
// Inverse Binding Event Handlers
public ActivityMainBindingImpl(#Nullable androidx.databinding.DataBindingComponent bindingComponent, #NonNull View root) {
this(bindingComponent, root, mapBindings(bindingComponent, root, 6, sIncludes, sViewsWithIds));
}
private ActivityMainBindingImpl(androidx.databinding.DataBindingComponent bindingComponent, View root, Object[] bindings) {
super(bindingComponent, root, 0
, (android.widget.Button) bindings[3]
, (android.widget.Button) bindings[5]
, (android.widget.EditText) bindings[4]
, (android.widget.TextView) bindings[2]
, (android.widget.TextView) bindings[1]
);
this.mboundView0 = (androidx.constraintlayout.widget.ConstraintLayout) bindings[0];
this.mboundView0.setTag(null);
this.textTemperatura.setTag(null);
this.textView.setTag(null);
setRootTag(root);
// listeners
invalidateAll();
}
#Override
public void invalidateAll() {
synchronized(this) {
mDirtyFlags = 0x2L;
}
requestRebind();
}
#Override
public boolean hasPendingBindings() {
synchronized(this) {
if (mDirtyFlags != 0) {
return true;
}
}
return false;
}
#Override
public boolean setVariable(int variableId, #Nullable Object variable) {
boolean variableSet = true;
if (BR.viewModel == variableId) {
setViewModel((com.example.convertitorecelsius_farenheit.MainViewModel) variable);
}
else {
variableSet = false;
}
return variableSet;
}
public void setViewModel(#Nullable com.example.convertitorecelsius_farenheit.MainViewModel ViewModel) {
this.mViewModel = ViewModel;
synchronized(this) {
mDirtyFlags |= 0x1L;
}
notifyPropertyChanged(BR.viewModel);
super.requestRebind();
}
#Override
protected boolean onFieldChange(int localFieldId, Object object, int fieldId) {
switch (localFieldId) {
}
return false;
}
#Override
protected void executeBindings() {
long dirtyFlags = 0;
synchronized(this) {
dirtyFlags = mDirtyFlags;
mDirtyFlags = 0;
}
java.lang.String viewModelTypeCurrentTemperature = null;
int viewModelConvertiTemperatura = 0;
com.example.convertitorecelsius_farenheit.MainViewModel viewModel = mViewModel;
if ((dirtyFlags & 0x3L) != 0) {
if (viewModel != null) {
// read viewModel.typeCurrentTemperature
viewModelTypeCurrentTemperature = viewModel.getTypeCurrentTemperature();
// read viewModel.convertiTemperatura()
viewModelConvertiTemperatura = viewModel.convertiTemperatura();
}
}
// batch finished
if ((dirtyFlags & 0x3L) != 0) {
// api target 1
this.textTemperatura.setText(viewModelConvertiTemperatura);
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.textView, viewModelTypeCurrentTemperature);
}
}
// Listener Stub Implementations
// callback impls
// dirty flag
private long mDirtyFlags = 0xffffffffffffffffL;
/* flag mapping
flag 0 (0x1L): viewModel
flag 1 (0x2L): null
flag mapping end*/
//end
}
Here's the program I wrote
MainActivity.java
package com.example.convertitorecelsius_farenheit;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProvider;
import android.os.Bundle;
import android.widget.EditText;
import com.example.convertitorecelsius_farenheit.databinding.ActivityMainBinding;
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
private MainViewModel viewModel;
public EditText inputTemperature;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
inputTemperature = findViewById(R.id.inputTemperatura);
binding = ActivityMainBinding.inflate(getLayoutInflater());
binding.setLifecycleOwner(this);
setContentView(binding.getRoot());
viewModel = new ViewModelProvider(this).get(MainViewModel.class);
binding.setViewModel(viewModel);
}
public int getInputTemperature() {
return Integer.parseInt(inputTemperature.toString());
}}
MainViewModel.java
package com.example.convertitorecelsius_farenheit;
import androidx.lifecycle.ViewModel;
public class MainViewModel extends ViewModel {
public int grades;
public boolean isCelsius = false;
MainActivity temperaturaInserita = new MainActivity();
//private final MutableLiveData<String> _TypeCurrentTemperatura = new MutableLiveData<>();
private String _TypeCurrentTemperatura = ""; //indicates if the temperature is celsius or farenheit
public String getTypeCurrentTemperature() {
return _TypeCurrentTemperatura;
}
public void changeTypeTemperature() {
if (isCelsius) {
isCelsius = false;
_TypeCurrentTemperatura = "F°";
} else {
isCelsius = true;
_TypeCurrentTemperatura = "C°";
}
}
public int convertiTemperatura() { //convertTemperature (that's the italian name)
if (isCelsius) {
grades = (int) ((temperaturaInserita.getInputTemperature() * 1.8) + 32);
} else {
grades = (int) ((int) ((temperaturaInserita.getInputTemperature()) -32) * .5556);
}
return grades;
}}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.example.convertitorecelsius_farenheit.MainViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="#+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="348dp"
android:text="#{viewModel.typeCurrentTemperature}"
android:textSize="24sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="#+id/cambiaTemperatura"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="80dp"
android:text="c--f"
app:layout_constraintBottom_toTopOf="#+id/textView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent" />
<EditText
android:id="#+id/inputTemperatura"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="52dp"
android:ems="10"
android:inputType="number"
app:layout_constraintBottom_toTopOf="#+id/cambiaTemperatura"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="#+id/converti"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="273dp"
android:layout_marginBottom="274dp"
android:text="converti"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/inputTemperatura"
app:layout_constraintVertical_bias="1.0" />
<TextView
android:id="#+id/textTemperatura"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="23dp"
android:text="#{viewModel.convertiTemperatura()}"
android:textSize="24sp"
app:layout_constraintBottom_toTopOf="#+id/textView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Can somebody help me :)
There are some things wrong in your code:
You're setting the activity view twice, remove the first line: setContentView(R.layout.activity_main);
Since you're using view binding (different thing from data binding), you dont need to call findViewById replace it with inputTemperature = binding.inputTemperatura
If you're already using data binding why bother with view binding? You can do all input/output related tasks in data binding.
You SHOULD NEVER instantiate ANDROID activities or hold a reference to it, this is task of the Android framework, remove the line MainActivity temperaturaInserita = new MainActivity(); of your viewmodel.
Check this answer it may help you: Android : Difference between DataBinding and ViewBinding
EDIT
You don't need to call methods of your activity from your viewmodel, this is a bad practice, because if the system destroys your activity you will end with a NPE in your view model, you have 2 options:
Use two way data binding to set/get the value of the temperature from the viewmodel: https://bignerdranch.com/blog/two-way-data-binding-on-android-observing-your-view-with-xml/
Change the function in the view model to receive as argument the value of the input text.
I recommend to go with first option, this way you will have the updated value always in your view model and also can survive config changes.
And remember don't matter what, your viewmodel SHOULD never have a reference to the Activity.
I have a fragment to display a queue of either videos of images. The video I display in the VideoView works fine, it replays, it's golden. But the images I put in the ImageView just appear invisible. I tried loading them through Uri, by reading a Bitmap, now they're set up with Picasso, and none of it fixed it. The AssetObtainer you'll see in MultimediaPlayer works with both sound files and videos so far, so I highly doubt it has an issue with images. Here's the code:
MultimediaPlayer.java :
public class MultimediaPlayer extends Fragment
{
VideoView mVideoView;
ImageView mImageView;
MultimediaViewModel mMultimediaViewModel;
Play mPlayThread;
Activity mActivity;
AssetObtainer assetObtainer = new AssetObtainer();
public Long mTutorialId;
public List<Multimedia> multimedias = new LinkedList<>();
#Override
public View onCreateView(#NotNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState)
{
return inflater.inflate(R.layout.fragment_multimedia_player, container, false);
}
#Override
public void onViewCreated(View view, Bundle savedInstanceState)
{
mActivity = requireActivity();
mMultimediaViewModel = new ViewModelProvider(this).get(MultimediaViewModel.class);
mVideoView = view.findViewById(R.id.video_embed);
mImageView = view.findViewById(R.id.image_embed);
getPlayer(0);
}
private void getPlayer(int position)
{
if(mPlayThread != null) {
mPlayThread.interrupt();
position++;
}
if(!multimedias.isEmpty()) {
mPlayThread = new Play(multimedias.get(position));
mPlayThread.start();
}
}
private class Play extends Thread
{
private final Multimedia currentMedia;
Play(Multimedia media){
currentMedia = media;
}
#Override
public void run()
{
int position = currentMedia.getPosition();
int displayTime = currentMedia.getDisplayTime();
boolean loopBool = currentMedia.getLoop();
if(currentMedia.getType()) {
mActivity.runOnUiThread(() -> {
mImageView.setVisibility(View.VISIBLE);
mVideoView.setVisibility(View.GONE);
try {
Picasso.get().load(assetObtainer.getFileFromAssets(requireContext(), currentMedia.getFullFileName())).into(mImageView);
} catch (IOException ignored) {}
});
if(displayTime>0) {
try {
sleep(displayTime);
if(!loopBool) multimedias.remove(currentMedia);
if(position<multimedias.size()-1) {
getPlayer(position);
} else getPlayer(0);
} catch (InterruptedException e) {
mActivity.runOnUiThread(() -> mImageView.setVisibility(View.GONE));
interrupt();
}
}
} else {
mActivity.runOnUiThread(() -> {
mVideoView.setVisibility(View.VISIBLE);
mImageView.setVisibility(View.GONE);
try {
mVideoView.setVideoURI(Uri.fromFile(assetObtainer.getFileFromAssets(requireContext(), currentMedia.getFullFileName())));
} catch (IOException ignored) {}
if(loopBool && multimedias.size()==1) mVideoView.setOnCompletionListener(v->getPlayer(position-1));
mVideoView.start();
});
}
}
}
#Override
public void onPause()
{
if(mPlayThread!=null){
mPlayThread.interrupt();
}
super.onPause();
}
}
bed for the fragment in the activity .xml file :
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/layout_multimedia"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="#id/active_instructions" />
and the .xml file of the fragment :
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:context=".tutorial.mediaplayer.MultimediaPlayer">
<VideoView
android:id="#+id/video_embed"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="#+id/image_embed"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
android:contentDescription="#string/image_default_no_description" />
</androidx.constraintlayout.widget.ConstraintLayout>
As mentioned, video works super fine, but images won't show. I'd appreciate even monkey wrench suggestions before I have to rework this entirely.
As mentioned in my other answer, it's not related to the ImageView, it's a problem with SQLite and loading a boolean from the database. The default false which I wanted to correspond with video types is default, so it worked, but since it doesn't default to true the image condition wasn't fulfilled. Anyway the ImageView will work once it is actually ran.
Lets say, in an Android app, we want to have the ability to temporarily and reliably ignore all user touches at any moment.
From the research I have done on stack-overflow as well as here, here, and here, the agreed-upon solution seems to be something like this:
(Code of MainActivity.java):
// returning true should mean touches are ignored/blocked
#Override
public boolean dispatchTouchEvent(MotionEvent pEvent) {
if (disableTouches) {
return true;
} else {
return super.dispatchTouchEvent(pEvent);
}
}
However, when we introduce the Android Monkey Exerciser Tool and send touch events to the app at a rapid rate, it becomes apparent that pigs begin to fly at the quantum level -- we can get calls to onClick() even after/during times where "blockTouches" has been set to true.
MY QUESTION IS: Why is that? -- Is this a normal Android behavior, or did I make a mistake in my code? :)
Note: I have already ruled out the possibility of onClick() being called by user input other than touches (and therefore being uncontrolled by the onDispatchTouchEvent() method)... by adding "—-pct-touch 100" to the monkey command.
Here is the code I am using for this test:
MainActivity:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
View rootView; // turns black when "touch rejection" is in progress
View allowedButton;
View notAllowedButton;
// Used to decide whether to process touch events.
// Set true temporarily when notAllowedButton is clicked.
boolean touchRejectionAnimationInProgress = false;
int errorCount = 0; // counting "unexpected/impossible" click calls
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
rootView = findViewById(R.id.rootView);
allowedButton = findViewById(R.id.allowedButton);
notAllowedButton = findViewById(R.id.notAllowedButton);
allowedButton.setOnClickListener(this);
notAllowedButton.setOnClickListener(this);
allowedButton.setBackgroundColor(Color.GREEN);
notAllowedButton.setBackgroundColor(Color.RED);
}
// returning true should mean touches are ignored/blocked
#Override
public boolean dispatchTouchEvent(MotionEvent pEvent) {
if (touchRejectionAnimationInProgress) {
Log.i("XXX", "touch rejected in dispatchTouchevent()");
return true;
} else {
return super.dispatchTouchEvent(pEvent);
}
}
#Override
public void onClick(View viewThatWasClicked){
Log.i("XXX", "onClick() called. View clicked: " + viewThatWasClicked.getTag());
//checking for unexpected/"impossible"(?) calls to this method
if (touchRejectionAnimationInProgress) {
Log.i("XXX!", "IMPOSSIBLE(?) call to onClick() detected.");
errorCount ++;
Log.i("XXX!", "Number of unexpected clicks: " + errorCount);
return;
} // else proceed...
if (viewThatWasClicked == allowedButton) {
// Irrelevant
} else if (viewThatWasClicked == notAllowedButton) {
// user did something that is not allowed.
touchRejectionAnimation();
}
}
// When the user clicks on something "illegal,"
// all user input is ignored temporarily for 200 ms.
// (arbitrary choice of duration, but smaller is better for testing)
private void touchRejectionAnimation() {
Log.i("XXX", "touchRejectionAnimation() called.");
touchRejectionAnimationInProgress = true;
rootView.setBackgroundColor(Color.BLACK);
// for logging/debugging purposes...
final String rejectionID = (new Random().nextInt() % 9999999) + "";
Log.i("XXX", "rejection : " + rejectionID + " started.");
Thread thread = new Thread(new Runnable() {
#Override
public void run() {
try { Thread.sleep(200); } catch (Exception e) {
Log.e("XXX", "exception in touchRejection() BG thread!");
}
runOnUiThread(new Runnable() {
#Override
public void run() {
Log.i("XXX", "rejection " + rejectionID + " ending");
rootView.setBackgroundColor(Color.WHITE);
touchRejectionAnimationInProgress = false;
}
});
}
});
thread.start();
}
}
layout.xml:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/rootView"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<View
android:id="#+id/allowedButton"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="32dp"
android:layout_marginBottom="32dp"
android:tag="allowedButton"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="#+id/notAllowedButton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="#+id/notAllowedButton"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="32dp"
android:layout_marginBottom="32dp"
android:tag="view2"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="#+id/allowedButton"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
If you don't want your onClick() to be triggered on any view click.
Following are the steps which need to take care.
Create custom viewGroup eg: MyConstraintLayout and add all child
inside it.
Override onInterceptTouchEvent(MotionEvent ev) and return it has true.
public class MyConstraintLayout extends ConstraintLayout {
private boolean mIsViewsTouchable;
public ParentView(Context context) {
super(context);
}
public ParentView(Context context, AttributeSet attrs) {
super(context, attrs);
inflate(context, R.layout.custom_view, this);
}
public ParentView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setViewsTouchable(boolean isViewTouchable) {
mIsViewsTouchable = isViewTouchable;
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mIsViewsTouchable;
}
}
Note: Use setViewsTouchable() method as per your requirement, If you pass the parameter as true all views are not clickable if false your views will be clickable.
I'm relatively new to Android development and I've been stuck on this for a couple of weeks.
I'm working with a JSON Array and I've been able to log that I am returning the JSON Array with an API call and that it is breaking up the array into separate JSON Objects.
Hero JSON {"PrimaryName":"Abathur","ImageURL":"Abathur","AttributeName":"Abat","Group":"Specialist","SubGroup":"Utility","Translations":"Abatur,АБАТУР,아바투르,阿巴瑟"}
Hero JSON {"PrimaryName":"Alarak","ImageURL":"Alarak","AttributeName":"Alar","Group":"Assassin","SubGroup":"Ambusher","Translations":"亞拉瑞克,阿拉纳克,알라라크,Аларак"}
etc...
From what it appears it is converting those JSON Objects into Java Objects.
hotsbuddy.HeroDataModel#f27ef8a
hotsbuddy.HeroDataModel#db939fb
etc...
When I changed the Log to return the Hero Name, Hero Image and Hero Group from my
public static HeroDataModel fromJson(JSONObject jsonObject)
method I get this:
03-24 18:44:53.828 30539-30539/com.timfreebernii.hotsbuddy D/HoTS: Hero Abathur Abathur Specialist
03-24 18:44:53.828 30539-30539/com.timfreebernii.hotsbuddy D/HoTS: Hero Alarak Alarak Assassin
03-24 18:44:53.828 30539-30539/com.timfreebernii.hotsbuddy D/HoTS: Hero Alexstrasza Alexstrasza Support
So I can see that I am getting Java Objects back.
These logs are being placed in my HeroDataModel class. However, I'm having an issue attaching these Java Objects to my ListView and ListView Adapter.
When I log my ArrayList creation in my API call I am getting Java Objects returned.
Array [com.timfreebernii.hotsbuddy.HeroDataModel#f27ef8a,
com.timfreebernii.hotsbuddy.HeroDataModel#db939fb, etc...]
These objects are not showing up in App View. I'm not getting a list just a blank white background with the blue bar with my app name from the Main Layout.
I've been using these guides from CodePath but I'm just not quite able to finish off this feature.
https://guides.codepath.com/android/Using-an-ArrayAdapter-with-ListView#row-view-recycling
https://guides.codepath.com/android/Converting-JSON-to-Models#bonus-setting-up-your-adapter
I know I'm using a different type of Adapter but I was kind of following along from a couple simple apps I built from a Udemy course I completed.
Here is the API I'm working with:
https://api.hotslogs.com/Public/Data/Heroes
My GitHub repo:
https://github.com/tfreebern2/hotsbuddy
Here is my HeroDataModel code:
public class HeroDataModel {
private String mHeroName;
private String mHeroImage;
private String mHeroGroup;
public String getHeroName() {
return this.mHeroName;
}
public String getHeroImage() {
return this.mHeroImage;
}
public String getHeroGroup() {
return this.mHeroGroup;
}
public static HeroDataModel fromJson(JSONObject jsonObject) {
HeroDataModel h = new HeroDataModel();
try {
h.mHeroName = jsonObject.getString("PrimaryName");
h.mHeroImage = jsonObject.getString("ImageURL");
h.mHeroGroup = jsonObject.getString("Group");
} catch (JSONException e) {
e.printStackTrace();
return null;
}
return h;
}
public static ArrayList<HeroDataModel> fromJson(JSONArray jsonObjects) {
JSONObject heroJson;
ArrayList<HeroDataModel> heroes = new ArrayList<HeroDataModel> . ();
for (int i = 0; i < jsonObjects.length(); i++) {
try {
heroJson = jsonObjects.getJSONObject(i);
// Log.d("HotS", "Hero JSON " + heroJson);
} catch (JSONException e) {
e.printStackTrace();
continue;
}
HeroDataModel hero = HeroDataModel.fromJson(heroJson);
// Log.d("HoTS", "Hero " + hero);
if (hero != null) {
heroes.add(hero);
}
}
// Log.d("HoTS", "Heroes Array" + heroes);
return heroes;
}
}
My HeroListAdapter:
public class HeroListAdapter extends ArrayAdapter<HeroDataModel> {
public HeroListAdapter(HeroListActivity context, ArrayList<HeroDataModel> heroes) {
super(context, 0, heroes);
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
HeroDataModel currentHero = getItem(position);
if (convertView == null) {
convertView = LayoutInflater.from(getContext()).inflate(
R.layout.hero_list_item, parent, false);
}
TextView heroNameView = convertView.findViewById(R.id.hero_name);
TextView heroImageView = convertView.findViewById(R.id.hero_image);
TextView heroGroupView = convertView.findViewById(R.id.hero_group);
heroNameView.setText(currentHero.getHeroName());
heroImageView.setText(currentHero.getHeroImage());
heroGroupView.setText(currentHero.getHeroGroup());
return convertView;
}
}
My HeroListActivity:
public class HeroListActivity extends AppCompatActivity {
final String HEROES_URL = "https://api.hotslogs.com/Public/Data/Heroes";
ArrayList<HeroDataModel> heroes;
HeroListAdapter adapter;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.hero_list);
heroListAPI(HEROES_URL);
ListView listView = (ListView) findViewById(R.id.lvHeroes);
heroes = new ArrayList<HeroDataModel>();
adapter = new HeroListAdapter(this, heroes);
listView.setAdapter(adapter);
}
private void heroListAPI(String url) {
AsyncHttpClient client = new AsyncHttpClient();
client.get(url, new JsonHttpResponseHandler() {
#Override
public void onSuccess(int statusCode, Header[] headers, JSONArray response) {
ArrayList<HeroDataModel> heroes = HeroDataModel.fromJson(response);
heroes.clear(); // clear existing items if needed
heroes.addAll(HeroDataModel.fromJson(response)); // add new items
adapter.notifyDataSetChanged();
}
#Override
public void onFailure(int statusCode, Header[] headers, Throwable e, JSONArray response) {
Log.e("HoTS", "Fail " + e.toString());
Log.d("HoTS", "Status code " + statusCode);
Toast.makeText(HeroListActivity.this, "Request Failed", Toast.LENGTH_SHORT).show();
}
});
}
}
My Hero_List Layout File:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/rlLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="${packageName}.${activityClass}" >
<ListView
android:id="#+id/lvHeroes"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true" >
</ListView>
</RelativeLayout>
My Hero_List_Item Layout File:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.timfreebernii.hotsbuddy.HeroActivity">
<TextView
android:id="#+id/hero_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hero Name"
/>
<TextView
android:id="#+id/hero_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hero Image"
/>
<TextView
android:id="#+id/hero_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hero Group"
/>
</LinearLayout>
Use Gson Library or retrofit to convert json objects into model class
Well I found a solution with some help.
My HeroListActivity is where I was making the API call and trying to convert the Json into Java Objects.
Here I declared my adapter in the HeroListActivity class outside of my onCreate method and removed the declaration of the ArrayList. I created a new HeroListAdapter using 'this' and 'new' ArrayList() as parameters. Then I attached the adapter to my ListView.
public class HeroListActivity extends AppCompatActivity {
final String HEROES_URL = "https://api.hotslogs.com/Public/Data/Heroes";
HeroListAdapter adapter;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.hero_list);
heroListAPI(HEROES_URL);
ListView listView = (ListView) findViewById(R.id.lvHeroes);
adapter = new HeroListAdapter(this, new ArrayList<HeroDataModel>());
listView.setAdapter(adapter);
}
... more code
}
In my onSuccess method I created an ArrayList named 'myHeroes' with my renamed fromJsonToModelList.
#Override
public void onSuccess(int statusCode, Header[] headers, JSONArray response) {
ArrayList<HeroDataModel> myHeroes = HeroDataModel.fromJsonToModelList(response);
adapter.clear();
adapter.addAll(myHeroes);
adapter.notifyDataSetChanged();
}
I clear the adapter of any previous data, add all objects to the adapter and notify the adapter of any changes.
Some of the changes were just refactoring suggestions.
Ultimately, I wasn't passing the data to my adapter correctly nor do I think creating it correctly in my onCreate method.
I have a RecyclerView with ImageViews in each item.
I set onClickListener for the ImageViews in onBindViewHolder as follows:
holder.starIV.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
// TODO: logic
}
});
The ripple effect worked fine until I added the following logic to onClick. This logic changes the Drawable for the ImageView.
holder.starIV.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (v.getId() == holder.starIV.getId()) {
ListItem clickedItem = mDataset.get(position);
ListItem updatedItem = new ListItem(clickedItem);
if (clickedItem.getStarState() == STAR_ON) {
updatedItem.setStarState(STAR_OFF);
updatedItem.setStarDrawable(
ContextCompat.getDrawable(
v.getContext(),R.drawable.ic_star_border_24px));
}
else if (clickedItem.getStarState() == STAR_OFF) {
updatedItem.setStarState(STAR_ON);
updatedItem.setStarDrawable(
ContextCompat.getDrawable(
v.getContext(),R.drawable.ic_star_24px));
}
mDataset.set(position,updatedItem);
notifyDataSetChanged();
}
}
});
Now, I get no ripple effect at all. Here's the XML for the ImageView:
<ImageView
android:id="#+id/list_item_star"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:paddingLeft="4dp"
android:paddingRight="16dp"
android:src="#drawable/ic_star_border_24px"
android:clickable="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:drawSelectorOnTop="true"
/>
The ripple effect works normally again when i comment out the logic part in onClick.
Have I implemented the above correctly?
What change would you suggest to get the ripple effect working correctly?
EDIT: It appears that changing the Drawable is interfering with the ripple animation. So i moved all the logic to an AsyncTask with a small delay to allow the animation to finish. This seems to work, but I feel this solution is not elegant. Here's the AsyncTask:
class DoLogix extends AsyncTask<Integer, Integer, Void> {
#Override
protected Void doInBackground(Integer... params) {
try{Thread.sleep(125);}catch (Exception e) {}
publishProgress(params[0]);
return null;
}
protected void onProgressUpdate(Integer... val) {
ListItem clickedItem = mDataset.get(val[0]);
ListItem updatedItem = new ListItem(clickedItem);
if (clickedItem.getStarState() == STAR_ON) {
updatedItem.setStarState(STAR_OFF);
updatedItem.setStarDrawable(starBorder);
}
else if (clickedItem.getStarState() == STAR_OFF) {
updatedItem.setStarState(STAR_ON);
updatedItem.setStarDrawable(star);
}
mDataset.set(val[0],updatedItem);
notifyDataSetChanged();
}
}
u can set a ripple drawable as the foreground of ur imageview.
add below code to your parent layout
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackgroundBorderless"