How can you change ViewPager2 position inside the ViewPager2Adapter? - java

I programmed a Vocabulary Trainer with Vocabulary Cards. The Vocabulary Cards are Entries in a Room Database created from an asset. I am displaying these Vocabulary Cards with ViewPager2 in an Activity. I have a 'correct' and a 'false' button and when the user clicks on either, I want to update the Vocabulary Card (-> The entry in the sqlite database) and automatically swipe to the next item of the ViewPager2.
If I implement the buttons in the ViewPager2Adapter, I can't find a way to change the position of the ViewPager2. If I implement the buttons in the activity the sqlite entry does not update properly (After it updates the entry, the activity is constantly refreshed, it seems like it never the leaves the OnClick methode of the button).
So is it possible to change the position of ViewPager2 from inside the ViewPager2Adpater?
Thanks for your help!
That is the relevant code if I have the buttons in my ViewPager2Adapter. Here I don't know how to change the position of the ViewPager2
public void onBindViewHolder(#NonNull #NotNull ViewHolder holder, int position) {
VocabularyCard vocabularyCard = currentCards.get(position);
holder.btn_correct.setOnClickListener(view -> {
vocabularyViewModel.updateSingleVocabularyCard(vocabularyCard);
});
holder.btn_false.setOnClickListener(v15 -> {
vocabularyViewModel.updateSingleVocabularyCard(vocabularyCard);
});
That is the relevant code if I have the buttons in the Activity. Here the update function triggers an infinite updating of the Activity:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
initAll();
btn_correct_2.setOnClickListener(view -> {
int currentPos = viewpager2.getCurrentItem();
vocabularyViewModel.getCurrentCards().observe(this, vocabularyCards -> {
if (vocabularyCards.size() == currentPos){
Intent intent = new Intent(TestActivity.this, MainActivity.class);
startActivity(intent);
}else {
viewpager2.setCurrentItem(currentPos + 1);
}
VocabularyCard vocabularyCard = vocabularyCards.get(currentPos);
vocabularyViewModel.updateSingleVocabularyCard(vocabularyCard);
});
});
btn_false_2.setOnClickListener(view -> {
int currentPos = viewpager2.getCurrentItem();
vocabularyViewModel.getCurrentCards().observe(this, vocabularyCards -> {
if (vocabularyCards.size() == currentPos){
Intent intent = new Intent(TestActivity.this, MainActivity.class);
startActivity(intent);
}else {
viewpager2.setCurrentItem(currentPos + 1);
}
VocabularyCard vocabularyCard = vocabularyCards.get(currentPos);
vocabularyViewModel.updateSingleVocabularyCard(vocabularyCard);
});
});
Objects.requireNonNull(getSupportActionBar()).setTitle(getResources().getString(R.string.learn_new_words));
LiveData<List<VocabularyCard>> allNewCards = vocabularyViewModel.getAllNewCards(goal);
allNewCards.observe(this, vocabularyCards -> vocabularyViewModel.setCurrentCards(vocabularyCards));
vocabularyViewModel.getCurrentCards().observe(this, vocabularyCards -> {
viewPager2Adapter.setCurrentCards(vocabularyCards);
viewpager2.setAdapter(viewPager2Adapter);
viewpager2.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
super.onPageScrolled(position, positionOffset, positionOffsetPixels);
}
#Override
public void onPageSelected(int position) {
super.onPageSelected(position);
}
#Override
public void onPageScrollStateChanged(int state) {
super.onPageScrollStateChanged(state);
}
});
});
The update function in the Room DAO is straightforward:
#Update
void updateSingleVocabularyCard(VocabularyCard vocabularyCard);
I left out all the code that is not relevant.

There are several ways to propagate an event from the adapter to the activity where you manage your cards using ViewPager2. Let's have a look how it can be done either using an interface or using the same view model. But in any case I strongly recommend you to update your database in a background thread to prevent any possible UI lags.
1. Using an interface
This option is more flexible since you can propagate events as well as pass data as parameters. You can also reuse this interface for other cases. As far as I See you have a holder that has 2 buttons for the users to make choices. So our event here would be something like ChoiceEventListener, let's call this interface like so. Then you'd have to add a method to handle this event from within anywhere you wanna hear this event, and let's call its handle method onChoice(). Finally we would need a variable to indicate what the choice is. Now that ready to implement, let's write the new interface...
ChoiceEventListener.java
public interface ChoiceEventListener {
void onChoice(VocabularyCard vocabularyCard, boolean choice);
}
The next thing to do is to implement this interface where you want to listen to this event. In this case it is in your activity. There are 2 ways to do this:
You make your activity to inherit its methods using the implements keyword
YourActivity.java
public class YourActivity extends AppCompatActivity implements ChoiceEventListener {
// Use a background thread for database operations
private Executor executor = Executors.newSingleThreadExecutor();
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
initAll();
// You must construct your adapter class with the listener
ViewPager2Adapter adapter = new ViewPager2Adapter(/* Other params... */, this);
}
#Override
public void onChoice(VocabularyCard vocabularyCard, boolean choice) {
if(choice) {
// User pressed the correct button
}
else {
// User pressed the false button
}
// Update card in the background
executor.execute(()-> vocabularyViewModel.updateSingleVocabularyCard(vocabularyCard));
}
}
You can implement it as an anonymous function
YourActivity.java
public class YourActivity extends AppCompatActivity {
// Use a background thread for database operations
private Executor executor = Executors.newSingleThreadExecutor();
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
initAll();
// You must construct your adapter class with the listener
ViewPager2Adapter adapter = new ViewPager2Adapter(/* Other params... */, (vocabularyCard, choice) -> {
if(choice) {
// User pressed the correct button
}
else {
// User pressed the false button
}
// Update card in the background
executor.execute(()-> vocabularyViewModel.updateSingleVocabularyCard(vocabularyCard));
});
}
}
Finally the ViewPager2Adapter class implementation would be something like this:
ViewPager2Adapter.java
public class ViewPager2Adapter extends RecyclerView.Adapter<ViewPager2ViewHolder> {
// Here is your listener to deliver the choice event to it
private final ChoiceEventListener listener;
// Constructor
public ViewPager2Adapter(/* Other params... */, ChoiceEventListener listener) {
/* Other inits */
this.listener = listener;
}
public void onBindViewHolder(#NonNull #NotNull ViewHolder holder, int position) {
VocabularyCard vocabularyCard = currentCards.get(position);
holder.btn_correct.setOnClickListener(view -> {
listener.onChoice(vocabularyCard, true); // true for correct
});
holder.btn_false.setOnClickListener(v15 -> {
listener.onChoice(vocabularyCard, false); // false for false :)
});
}
}
2. Use the ViewModel for inter-communication
In this option we use a LiveData object to make page switching. The only thing you need to know in your activity is the current position which you get it from the adapter class. Once you update it in the adapter, set the current position value in live data so that you can switch the page in your activity.
VocabularyViewModel.java
public class VocabularyViewModel extends ViewModel {
public MutableLiveData<Integer> mldCurrentPosition = new MutableLiveData<>(0);
}
YourActivity.java
public class YourActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
initAll();
vocabularyViewModel.mldCurrentPosition().observe(this, currentPosition -> {
if(currenPosition == null) return; // ignore when null
viewpager2.setCurrentItem(currentPosition + 1);
}
}
}
Finally the ViewPager2Adapter class implementation would be something like this:
ViewPager2Adapter.java
public class ViewPager2Adapter extends RecyclerView.Adapter<ViewPager2ViewHolder> {
// Use a background thread for database operations
private Executor executor = Executors.newSingleThreadExecutor();
public void onBindViewHolder(#NonNull #NotNull ViewHolder holder, int position) {
VocabularyCard vocabularyCard = currentCards.get(position);
holder.btn_correct.setOnClickListener(view -> {
// Update card in the background
executor.execute(()-> vocabularyViewModel.updateSingleVocabularyCard(vocabularyCard));
// Then invoke switching to the next card
vocabularyViewModel.mldCurrentPosition.setValue(position + 1);
});
holder.btn_false.setOnClickListener(v15 -> {
// Update card in the background
executor.execute(()-> vocabularyViewModel.updateSingleVocabularyCard(vocabularyCard));
// Then invoke switching to the next card
vocabularyViewModel.mldCurrentPosition.setValue(position + 1);
});
}
}

Related

How to detect each RecyclerView item after it is displayed

I want to detect each item in my RecylerView after it is displayed to the user.
Basically, I am trying to play a sound on each item after it is loaded on the screen.
But I am not able to detect whenever each item is loaded on the screen! Is there any method I have to call to detect each item rendered
E.g 1st RecyclerView item displayed -> play sound
2st RecyclerView item displayed -> play sound...
My Adapter class looks like this -
public class AdapterListAnimation extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private List<Multiples> items = new ArrayList<>();
private Context ctx;
private OnItemClickListener mOnItemClickListener;
private int animation_type = 0;
.........
.........
I am calling this initComponent() method from onCreated() method. Can you give advice on what should I do to achieve my goal as described above?
private void initComponent() {
recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setHasFixedSize(true);
items = DataGenerator.getPeopleData(this,of,value);
setAdapter();
/* MediaPlayer mp=MediaPlayer.create(this, R.raw.sword);
if (mp.isPlaying()) {
mp.stop();
mp.release();
mp = MediaPlayer.create(this, R.raw.sword);
} mp.start();*/
}
private void setAdapter() {
// Set data and list adapter
mAdapter = new AdapterListAnimation(this, items, animation_type);
recyclerView.setAdapter(mAdapter);
// on item list clicked
mAdapter.setOnItemClickListener(new AdapterListAnimation.OnItemClickListener() {
#Override
public void onItemClick(View view, com.math.multiplication.model.Multiples obj, int position) {
Snackbar.make(parent_view, "Item " + obj.first + " clicked", Snackbar.LENGTH_SHORT).show();
}
});
}
you need to override onViewAttachedToWindow and onViewDetachedFromWindow. but for detecting holer type you need getItemViewType() just like that:
public class PostAdapter extends RecyclerView.Adapter {
#Override
public int getItemViewType(int position) {
switch (types.get(position)){
case 1:
return 1;
case 2:
return 2;
default:
return position;
}
}
#Override
public void onViewAttachedToWindow(#NonNull RecyclerView.ViewHolder holder) {
super.onViewAttachedToWindow(holder);
if (holder.getItemViewType() == 1){
//play song
}
}
#Override
public void onViewDetachedFromWindow(#NonNull RecyclerView.ViewHolder holder) {
super.onViewDetachedFromWindow(holder);
if (holder.getItemViewType() == 1){
//pause song
}
}
You need to add listener for TTS. Then update your RecyclerView to show right image when speech starts and ends.
I've created a test project to show how it can be implemented. Here you can see how it works. Here is my github repository.
Here is main part of my MainActivity class.
private void initTts() {
tts = new TextToSpeech(this, this);
tts.setLanguage(Locale.US);
tts.setOnUtteranceProgressListener(new MyListener());
}
#Override
public void onInit(int status) {
playSound(0);
}
private void playSound(int index) {
HashMap<String, String> hashMap = new HashMap<>();
hashMap.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, String.valueOf(index));
tts.speak(data.get(index), TextToSpeech.QUEUE_ADD, hashMap);
}
class MyListener extends UtteranceProgressListener {
#Override
public void onStart(String utteranceId) {
int currentIndex = Integer.parseInt(utteranceId);
mMainAdapter.setCurrentPosition(currentIndex);
handler.post(new Runnable() {
#Override
public void run() {
mMainAdapter.notifyDataSetChanged();
}
});
}
#Override
public void onDone(String utteranceId) {
int currentIndex = Integer.parseInt(utteranceId);
mMainAdapter.setCurrentPosition(-1);
handler.post(new Runnable() {
#Override
public void run() {
mMainAdapter.notifyDataSetChanged();
}
});
if (currentIndex < data.size() - 1) {
playSound(currentIndex + 1);
}
}
#Override
public void onError(String utteranceId) {
}
}
You can simply use onViewAttachedToWindow(VH) in your adapter.
See https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html#onViewAttachedToWindow(VH)
Update:
As you know RecyclerView will be call OnBindViewHolder only once for each item.
RecyclerView will not call this method again if the position of the item changes in the data set unless the item itself is invalidated or the new position cannot be determined.
So you can use onViewAttachedToWindow
(onViewAttachedToWindow) Called when a view created by this adapter has been attached to a window.
This can be used as a reasonable signal that the view is about to be seen by the user. If the adapter previously freed any resources in onViewDetachedFromWindow those resources should be restored here.
You can use it like this:
public class Adapter extends RecyclerView.Adapter<MyViewHolder> {
// rest of your code
#Override
public void onViewAttachedToWindow(MyViewHolder holder) {
super.onViewAttachedToWindow(holder);
// play your sound
}
}
Hope it helps!
If I understood your problem correctly, you want to play a sound when an item in the RecyclerView is loaded.
Hence I would like to think of a workaround here. You might consider having an item added after the sound for the previous item has been played.
I would like to provide a pseudo code for what I am trying to explain. You might consider using the MediaPlayer.OnCompletionListener for listening if your sound has stopped playing and then add the next item to the RecyclerView and then call notifyDataSetChanged on your adapter to load the next item in your RecyclerView.
Hence you might want the following modification in your adapter first.
// Declare a function in your adapter which adds new item in the RecyclerView
public void addItem(Multiples item) {
items.add(item);
notifyItemInserted(items.size());
}
Now in your Activity where you set up the RecyclerView, you need to have the following arrays.
ArrayList<Multiples> allItems = new ArrayList<Multiples>(); // This list will contain all the data
int soundToBePlayed = -1; // Initialize the value with -1
ArrayList<Integer> soundList = getAllSoundsInAList();
Modify your functions as follows.
private void initComponent() {
recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setHasFixedSize(true);
allItems = DataGenerator.getPeopleData(this,of,value);
setAdapter();
}
private void setAdapter() {
mAdapter = new AdapterListAnimation(this, animation_type); // Do not pass the items list
recyclerView.setAdapter(mAdapter);
// .. Your item click listener goes here
}
As the items was removed from the constructor, you need to modify the constructor of your adapter as well. Just remove the variable which takes the items. Because we will be adding elements to the items later.
Then you need to write another function which needs to be called in your onCreate function of your activity after your call your initComponent function. The function should look like the following.
private void addItemInRecyclerViewAndPlaySound() {
soundToBePlayed++;
adapter.addItem(allItems.get(soundToBePlayed));
recyclerView.scrollToPosition(adapter.getItemCount() - 1);
Integer soundId = soundList.get(soundToBePlayed);
MediaPlayer mp = MediaPlayer.create(this, soundId);
if (mp.isPlaying()) {
mp.stop();
mp.release();
mp = MediaPlayer.create(this, soundId);
mp.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
public void onCompletion(MediaPlayer mp) {
// Call this function again to add next item
// and play the next sound at the same time
if(soundToBePlayed < allItems.size() - 1)
addItemInRecyclerViewAndPlaySound();
}
});
}
mp.start();
}
I have not tested the code, however, I think you get the idea already. This can be implemented gracefully using a interface inside the adapter as well. The imeplemntation technique is your choice.

Custom ListView has a Button - How to detect which row the button was pressed on? [duplicate]

This question already has answers here:
Android : How to set onClick event for Button in List item of ListView
(10 answers)
Closed 4 years ago.
I'm making an inventory tracker app for Android for a practice project. I use SQLite for the storage and my ListView displays the contents of the database using a CursorAdapter. I use CursorLoader to fetch the data from the database
Each row in my ListView has a couple of TextViews and a Button. I plan to use the Button to decrement the quantity column/property of the selected item in the database.
Where do I setup the button OnClick listener? In my Activity class or my CursorAdapter class' bindView()?
Also how can I detect which row the button was pressed on from the button click?
I've already used the ListView's onItemClickListener to send the user to a detailed Activity that display more info about the current row. That had an id argument that gets passed. So I'm finding an equivalent that I can use for the buttons I put on each row.
Look the following. I used the Custom adapter to add the onItemClickListener:
private class MyListAdapter(val ctx: Context, val values: List<MyListInformatin>) : ArrayAdapter<MyListInformatin>(ctx, -1, values) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
val rowView = inflater.inflate(R.layout.item_my_layout, parent, false)
rowView.setOnClickListener {
val detailsActivity = Intent(ctx, DetailsActivity::class.java)
detailsActivity.putExtra(DetailsActivity.ROUTE_NUMBER_PARAM, values.get(position).routeNumber)
val bundle = Bundle()
bundle.putParcelable(detailsActivity.ROUTE_NO_STRUCTURE_PARAM, values.get(position).strucutre)
detailsActivity.putExtra(detailsActivity.PARAM_BUNDLE, bundle)
ctx.startActivity(detailsActivity)
}
return rowView
}
}
You can create an interface inside your adapter and a declare a fucntion in this interfae like ** void itemClick(int position)** and implement that interface in your activity
for void itemClick(int position) method :- Define it on
holder.setOnClickListener() block like -
viewHolder.button.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
if (yourInterface != null) {
yourInterface.itemClick(position);
}
}
});
For detailed description refer the below link -
https://www.c-sharpcorner.com/UploadFile/9e8439/create-custom-listener-on-button-in-listitem-listview-in-a/
This question has been answered already, you need to setup your list items as not clickable and manually listen for onClick events of buttons, see a detailed answer here: Android : How to set onClick event for Button in List item of ListView
public class MyCustomAdapter extend Adapter {
private MyOnClickListener mListener;
#Override
public View getView(int position, View view, ViewGroup parent)
{
...
// Your view
mContainer.setOnClickListener(new View.OnClickListener()
{
#Override
public void onClick(View v)
{
mListener.onWholeItemClick(position);
}
});
// Your button
mButton.setOnClickListener(new View.OnClickListener()
{
#Override
public void onClick(View v)
{
mListener.onButtonClick(position);
}
});
}
public void setListener(MyOnClickListener listener) {
this.mListener = listener;
}
// Listener interface
public interface MyOnClickListener() {
void onWholeItemClick(int position); // Can pass data as a parametter
void onButtonClick(int position);
}
}
public class MyActivity extend Activity implement MyOnClickListener() {
...
#Override
public void onCreate(...) {
mAdapter = new MyCustomAdapter(this);
mAdapter.setListener(this); // Pass the listener to adapter
}
#Override
public void onWholeItemClick(int position) {
//
}
#Override
public void onButtonClick(int position) {
//
}
}
That how you can fully hanhle click events
In onBindViewHolder give a tag to that button. that tag can be the position of item or the item itself . ( tag can be object )
override fun onBindViewHolder(holder: ItemHolder, position: Int) {
CUSTOMMODEL item = items[position];
...
holder.button.setTag(item);
//OR
holder.button.setTag(position);
...
holder.button.setOnClickListener(clickListener);
}
in your onClick(View view) method, get tag from the view and you can understand which item is clicked.

PagerAdapter changed adapters content with notifying

So first things first, here's the error that I'm getting:
java.lang.IllegalStateException: The application's PagerAdapter
changed the adapter's contents without calling
PagerAdapter#notifyDataSetChanged! Expected adapter item count: 18,
found: 28
I'm using a RecyclerView in my main activity and that has a List<Objects> as the dataset, that's all fine.
From that activity I call the second activity when a RecyclerView item is clicked that is basically a gallery implemented using a ViewPager using this code:
public void startSlideActivity(final int position) {
DataTransferer.get().storeItems(feed);
Intent i = new Intent(context, SlideActivity.class);
...
i.putExtra("POSITION", position);
context.startActivity(i);
}
My data is too large to transfer through an intent (using Parcelable or otherwise) so I'm using a singleton to hold and transfer my list, here's the code:
public class DataTransferer {
private static volatile DataTransferer singleton;
private List<Thing> items;
public static DataTransferer get(){
if (singleton == null) {
synchronized (DataTransferer.class) {
singleton = new DataTransferer();
}
}
return singleton;
}
private DataTransferer(){
}
public void storeItems(List<Thing> items){
this.items = items;
}
public List<Thing> getStoredItems(){
return items;
}
}
In the second activity I set the adapter and retrieve the list like so:
feed = DataTransferer.get().getStoredItems();
final int position = this.getIntent().getIntExtra("POSITION", 0);
adapter = new FeedPagerAdapter(SlideActivity.this, feed);
viewPager.setAdapter(adapter);
adapter.notifyDataSetChanged();
viewPager.setCurrentItem(position);
viewPager.setOffscreenPageLimit(4);
And finally here's in my PagerAdapter code:
public class FeedPagerAdapter extends PagerAdapter {
#BindView(R.id.item_image_view) ImageView image;
private final SlideActivity host;
private List<Thing> items;
public FeedPagerAdapter(SlideActivity host, List<Thing> items){
this.host = host;
this.items = items;
notifyDataSetChanged();
}
#Override
public Object instantiateItem(ViewGroup parent, int position) {
View view = LayoutInflater.from(host).inflate(R.layout.item, parent, false);
ButterKnife.bind(this, view);
...
parent.addView(view);
return view;
}
#Override
public int getCount() {
return items.size();
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View)object);
}
#Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
}
I've tried notifying the dataset in onResume and onPause and getItemCount in the adapter also, same problem.
Back to the main activity, this data is loaded over the network and adds items to the list when the load finishes. If I start my application and click on the item as soon they start to populate the RecyclerView, it opens the second activity and I get the crash. If I start the app and wait a second and click a RecyclerView item, it works as intended.
If anyone has any suggestions on how I can wait to make sure the list is stable when starting the second activity or a better way to implement a grid based RecyclerView gallery to open a viewpager type layout with the same dataset I would really appreciate it.
It's because you are using this line viewPager.setOffscreenPageLimit(4);, It's mean viewpager won't re-create the screen in all 4 pages. However, there is a function to detect if your screen has been visible completely, it's call setUserVisibleHint(). You just need to use like below:
//For the Fragment case
#Override
public void setUserVisibleHint(boolean isVisibleToUser) {
if (isVisibleToUser) {
//TODO notify your recyclerview data over here
}
}
EDIT:
For the Activity case: If targeting API level 14 or above, one can use
android.app.Application.ActivityLifecycleCallbacks
public class MyApplication extends Application implements ActivityLifecycleCallbacks {
private static boolean isInterestingActivityVisible;
#Override
public void onCreate() {
super.onCreate();
// Register to be notified of activity state changes
registerActivityLifecycleCallbacks(this);
....
}
public boolean isInterestingActivityVisible() {
return isInterestingActivityVisible;
}
#Override
public void onActivityResumed(Activity activity) {
if (activity instanceof MyInterestingActivity) {
isInterestingActivityVisible = true;
}
}
#Override
public void onActivityStopped(Activity activity) {
if (activity instanceof MyInterestingActivity) {
isInterestingActivityVisible = false;
}
}
// Other state change callback stubs
....
}
Register your application class in AndroidManifest.xml:
<application
android:name="your.app.package.MyApplication"
android:icon="#drawable/icon"
android:label="#string/app_name" >
Add onPause and onResume to every Activity in the project:
#Override
protected void onResume() {
super.onResume();
MyApplication.activityResumed();
}
#Override
protected void onPause() {
super.onPause();
MyApplication.activityPaused();
}
In your finish() method, you want to use isActivityVisible() to check if the activity is visible or not. There you can also check if the user has selected an option or not. Continue when both conditions are met.

Dynamic UI Progress Updates using Interface + ExecutorService Issue

So I have a music application. I am trying to update the UI with the progress of the media player (like current time, current song, album cover) everytime the song changes. I found that using interfaces was a awesome magical way of communication between activity and fragments so I implemented an interface in my MusicManger class. My code will show what and how did it.
Two problems
1) Commented look below, ExecutorService seems to stop after one loop. No Errors in catch block (this is why I tagged with java)
2) Commented please look, All the System.out methods print but the UI doesn't update. I do believe I called the method from mainThread so it should update.
I'll show code in logical order will add titles in bold before code segment to tell you basic idea of code.
Passing UI references from fragment to MusicManager class, code below in Fragment class
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_item_songlist, container, false);
// Set the adapter
TextView musicManagerSongName, musicManagerCurrent, musicManagerTotal;
ProgressBar musicManagerProgress;
ImageView musicManagerImageView;
mListView = (AbsListView) view.findViewById(R.id.slist);
musicManagerSongName = (TextView)view.findViewById(R.id.textView12);
musicManagerCurrent = (TextView)view.findViewById(R.id.textView10);
musicManagerTotal = (TextView)view.findViewById(R.id.textView11);
musicManagerProgress = (ProgressBar)view.findViewById(R.id.progressBar);
musicManagerImageView = (ImageView)view.findViewById(R.id.imageView2);
MainActivity.mediaPlayer.passUIReferences(musicManagerSongName, musicManagerCurrent, musicManagerTotal, musicManagerProgress, musicManagerImageView, view);
// line above is a method within MusicManager that takes the references will show code next!
ImageButton playbutton = (ImageButton)view.findViewById(R.id.playbuttonbar);
ImageButton nextButton = (ImageButton)view.findViewById(R.id.nextbuttonbar);
ImageButton backButton = (ImageButton)view.findViewById(R.id.backbuttonbar);
ImageButton toggleButton = (ImageButton)view.findViewById(R.id.shufflebuttonbar);
ImageButton pausebutton = (ImageButton)view.findViewById(R.id.pausebuttonbar);
playbutton.setBackgroundResource(R.drawable.playbuttonbar);
playbutton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
try {
MainActivity.mediaPlayer.stateChange(1);
}catch(Exception e) {
}
}
});
backButton.setBackgroundResource(R.drawable.backbutton1);
nextButton.setBackgroundResource(R.drawable.nextbutton1);
toggleButton.setBackgroundResource(R.drawable.shufflebuttonselected);
pausebutton.setBackgroundResource(R.drawable.pausebutton1);
pausebutton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
try {
MainActivity.mediaPlayer.stateChange(0);
} catch (Exception e){
}
}
});
mListView.setAdapter(mAdapter);
((MainActivity) mListener).restoreActionBar();
return view;
}
As Commended above the code that is located in MusicManager class that takes references and stores them. Also shows interface implementation with MusicManager class. And the Executor service
public void passUIReferences(View... views) {
this.uiElements = views;
}
private ExecutorService executorService = Executors.newSingleThreadExecutor();
private MediaplayerUpdateInterface uiUpdateInterface;
public MediaPlayerManager(MediaplayerUpdateInterface inter) {
this.player = new MediaPlayer();
this.uiUpdateInterface = inter;
// The below line starts the single thread while loop for excutorservice and only loops and prints "this" once after I start one song then it never loops again
executorService.submit(new Runnable() {
#Override
public void run() {
while(true) {
if (player.isPlaying() && uiElements != null) {
System.out.println("this");
uiUpdateInterface.updateUI(uiElements, 0);
}
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
}
}
});
}
public interface MediaplayerUpdateInterface {
public void updateUI(View[] views, int type);
}
Finally some code from MainActivity class that actually is suppose to update the UI note that both println's work as expected but only once as stated above because of the executorservice issue
public static MediaPlayerManager mediaPlayer = new MediaPlayerManager(new MediaPlayerManager.MediaplayerUpdateInterface() {
#Override
public void updateUI(View[] views, int type) {
System.out.println("check1 " + type);
updateMediaplayerViews(views, type);
}
});
private static void updateMediaplayerViews(View[] views, int type)
{
switch(type) {
case 0:
System.out.println("that?");
((TextView)views[0]).setText(mediaPlayer.getCurrentSongInfo().getName().length() > 22? mediaPlayer.getCurrentSongInfo().getName().substring(0, 19)+"..." : mediaPlayer.getCurrentSongInfo().getName());
break;
}
views[views.length - 1].invalidate();
}
The view array is shown perviously! Also the last view in the array is shown as the main view for songlist fragment.
I am sorry for all the code I've tried to debug it as you can see from my println's there is just something I am unaware of going on here.
Ok so there was an error that I needed to catch to see within the following code:
private static void updateMediaplayerViews(View[] views, int type)
{
switch(type) {
case 0:
System.out.println("that?");
((TextView)views[0]).setText(mediaPlayer.getCurrentSongInfo().getName().length() > 22? mediaPlayer.getCurrentSongInfo().getName().substring(0, 19)+"..." : mediaPlayer.getCurrentSongInfo().getName());
break;
}
views[views.length - 1].invalidate();
}
The issue is I was trying to change the view from a different thread then the one which created it. Solving it was pretty long and painful but basically I made it nonstactic used more interfaces then used the famous
Mainactivity.runOnUiThread(new Runnable(....));

Android communication between main thread and other thread

I want to change dynamically the text of a textview, but I will need the same logic if I want to make a game thread, so I need to make the communication between the main one and the second one.
I have the files :
MainActivity
public class MainActivity extends ActionBarActivity {
public static Handler mHandler;
Runnable thread = new SampleThread();
TextView txt1 = (TextView) findViewById(R.id.txt1);
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActionBar actionBar = getSupportActionBar();
actionBar.hide();
//hiding status bar
if (Build.VERSION.SDK_INT < 16) {
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
} else {
View decorView = getWindow().getDecorView();
int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN;
decorView.setSystemUiVisibility(uiOptions);
}
setContentView(R.layout.activity_main);
mHandler = new Handler() {
public void handleMessage(Message msg) {
// process incoming messages here
// i want to change the text of txt1 here
}
};
new Thread(thread).start();
}
}
SampleThread
package com.example.katsar0v.myapplication;
import android.util.Log;
/**
* Created by Katsar0v on 1/21/2015.
*/
public class SampleThread implements Runnable {
#Override
public void run() {
int two = 0;
while(two<10) {
two++;
try {
Thread.sleep(1000);
//instead of logging, i want to send the text to main UI
Log.d("MSG", String.valueOf(two + "sec"));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
The problem I see is, how do I change the text with the handler, when my thread is in another file? Or should I make the second class static within the first one (and what should I do when the code gets really long, it can't be all in one file)?
You could implement a custom Interface in order to handle it from your main activity.
On your SampleThread:
public interface TextViewChangeListener
{
public void onTextViewChanged(String newName);
}
TextViewChangeListener mListener;
Then call mListener.onTextViewChanged(String newName) wherever you want to have the new name in your TextView. Remember to initialize mListener with an instance of your MainActivity first, otherwise you will get a null pointer exception. You can do that either in the constructor of SampleThread or by creating a method for the purpose.
In your activity you should implement SampleThread.TextViewChangeListener and override the onTextViewChanged.
#Override
public void onTextViewChanged(String newName)
{
//MyTextView.setText(newName);
}
Edit: untested code:
MainActivity:
public class MainActivity extends ActionBarActivity implements SampleThread.TextViewChangeListener {
#Override
public void onTextViewChanged(Message msg)
{
// process incoming messages here
// i want to change the text of txt1 here
}
public static Handler mHandler;
Runnable thread = new SampleThread(this);
TextView txt1 = (TextView) findViewById(R.id.txt1);
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActionBar actionBar = getSupportActionBar();
actionBar.hide();
//hiding status bar
if (Build.VERSION.SDK_INT < 16) {
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
} else {
View decorView = getWindow().getDecorView();
int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN;
decorView.setSystemUiVisibility(uiOptions);
}
setContentView(R.layout.activity_main);
new Thread(thread).start();
}
}
SampleThread:
package com.example.katsar0v.myapplication;
import android.util.Log;
/**
* Created by Katsar0v on 1/21/2015.
*/
public class SampleThread implements Runnable
{
public interface TextViewChangeListener
{
public void onTextViewChanged(Message msg);
}
public SampleThread(TextViewChangeListener mListener)
{
this.mListener = mListener;
}
TextViewChangeListener mListener;
#Override
public void run() {
int two = 0;
while(two<10) {
two++;
try {
Thread.sleep(1000);
mListener.onTextViewChanged(String.valueOf(two + "sec"));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Let me know if that helped.
You can find some examples in Grafika, which does a lot of work off the UI thread. For example, TextureFromCameraActivity has a pair of handlers, one for the UI thread, one for the renderer thread. In onResume() you can see the main thread passing its handler to the renderer through a constructor, then retrieving the renderer thread's handler with a method call.
ContinuousCaptureActivity has a slightly different approach, using a Handler that also implements a callback interface. The handler object is passed to the CircularEncoder constructor as an interface instance. The public callback methods use the Handler internally.
The only tricky bit is if you're passing a Handler out of the non-UI thread. You either need to do it before the thread starts, or use appropriate thread synchronization operations to avoid data races.
You don't need to have your classes in the same file (and you really shouldn't unless one is nested inside the other). If they're in the same package then the default (package) scope will let them see each other. The first example from Grafika uses nested / private classes, the second example is more spread out.
Of course, if all you're trying to do is submit UI events from a non-UI thread, you can just use Activity.runOnUiThread().

Categories

Resources