I am trying to create a timer for user-generated posts that is displayed on the posts. I am using Firestore to store all of the information about the posts.
The code I have does display the timer but when multiple posts are made by multiple users the timer flashes between the correct time and another count down which is unrelated as far as I can see. Here is what I have:
This is how I create each post.
post.setTimer(data.getStringExtra("timerDuration"));
Calendar cal = Calendar.getInstance();
cal.add(Calendar.SECOND, setEndDate());
Date endtime = cal.getTime();
post.setTimerEndDate(endtime);
addPhotoInfoToDatabase();
finish();
My Adapter for the Fragment:
adapter = new FirestoreRecyclerAdapter<Post, PostViewHolder>(options) {
#Override
protected void onBindViewHolder(#NonNull PostViewHolder postViewHolder, int position, #NonNull Post post) {
postViewHolder.setPost(post);
}
#NonNull
#Override
public PostViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.card_view_layout, parent, false);
return new PostViewHolder(view);
}
};
If there is more information you need please let me know. Thank you for any help.
Edit:
My app also contains a ViewPager and I have noticed that when I swipe away from the problem timer and swipe back the issue is fixed.
Update:
void setPost(final Post post) {
ImageView imageView1 = view.findViewById(R.id.firstImageCardView);
ImageView imageView2 = view.findViewById(R.id.secondImageCardView);
ImageView profilePic = view.findViewById(R.id.user_image);
TextView username = view.findViewById(R.id.user_name);
final TextView timer = view.findViewById(R.id.timer);
final Button chooseRight = view.findViewById(R.id.choose_right);
final Button chooseLeft = view.findViewById(R.id.choose_left);
final Button follow = view.findViewById(R.id.follow);
String displayName;
final String userId = currentUser.getUid();
final String postId = post.getUserId();
setProfilePicture(post);
final DocumentReference docRefUsers = db
.collection("users")
.document(currentUser.getUid());
final DocumentReference docRefFollowing = db
.collection("following")
.document(currentUser.getUid())
.collection("UserIsFollowing")
.document(post.getUserId());
final DocumentReference docRefPosts = db
.collection("posts")
.document(post.getUserId())
.collection("userPosts")
.document(post.getDate().toString());
if (userId.equals(postId)) {
displayName = "Me";
chooseLeft.setEnabled(false);
chooseRight.setEnabled(false);
follow.setText("");
follow.setEnabled(false);
} else if (post.getUsername() == null) {
displayName = "Anonymous";
} else {
displayName = post.getUsername();
}
if (post.getProfilePicture() != null) {
Picasso.get().load(post.getProfilePicture())
.transform(new CircleTransformActivity())
.fit()
.centerCrop()
.into(profilePic);
username.setText(displayName);
} else {
Picasso.get().load(R.drawable.blank_profile_pic)
.transform(new CircleTransformActivity())
.fit()
.into(profilePic);
}
/***********************************************************/
if(post.getTimerEndDate() != null) {
Date date = java.util.Calendar.getInstance().getTime();
long currentTime = date.getTime();
long endTime = post.getTimerEndDate().getTime();
long timeLeft = endTime - currentTime;
new CountDownTimer(timeLeft, 1000) {
#SuppressLint("DefaultLocale")
public void onTick(long millisUntilFinished) {
timer.setText(String.format("%02d:%02d:%02d",
(int) ((millisUntilFinished / (1000 * 60 * 60)) % 24),
(int) ((millisUntilFinished / (1000 * 60)) % 60),
(int) (millisUntilFinished / 1000) % 60));
//here you can have your logic to set text to edittext
}
public void onFinish() {
timer.setText("done!");
}
}.start();
}
Picasso.get()
.load(post.getImageUrl_1())
.fit()
.transform(new RoundedCornersTransformation(30, 30))
.into(imageView1);
Picasso.get()
.load(post.getImageUrl_2())
.fit()
.transform(new RoundedCornersTransformation(30, 30))
.into(imageView2);
if (!postId.equals(userId)) {
docRefFollowing.addSnapshotListener(new EventListener<DocumentSnapshot>() {
#Override
public void onEvent(#Nullable DocumentSnapshot documentSnapshot, #Nullable FirebaseFirestoreException e) {
if (documentSnapshot != null && documentSnapshot.exists()) {
follow.setText("Following");
follow.setEnabled(false);
}
}
});
}
follow.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
final Map<String, Object> following = new HashMap<>();
following.put("Exists", true);
docRefFollowing.get().addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() {
#Override
public void onComplete(#NonNull Task<DocumentSnapshot> task) {
if (task.isSuccessful()) {
docRefFollowing.set(following);
docRefUsers.update("following", FieldValue.increment(1));
}
}
});
db.collection("users")
.document(postId)
.update("followers", FieldValue.increment(1));
follow.setText("Following");
}
});
if (!postId.equals(userId)) {
docRefPosts.collection("voted")
.document(currentUser.getUid()).addSnapshotListener(new EventListener<DocumentSnapshot>() {
#Override
public void onEvent(#Nullable DocumentSnapshot documentSnapshot, #Nullable FirebaseFirestoreException e) {
if (documentSnapshot != null && documentSnapshot.exists()) {
whichVoted = documentSnapshot.get("votedFor").toString();
}
if (whichVoted != null) {
switch (whichVoted) {
case ("left"):
chooseLeft.setEnabled(false);
chooseRight.setEnabled(true);
break;
case ("right"):
chooseRight.setEnabled(false);
chooseLeft.setEnabled(true);
break;
}
} else {
chooseRight.setEnabled(true);
chooseLeft.setEnabled(true);
}
}
});
}
chooseLeft.setText(post.getLeftText());
chooseRight.setText(post.getRightText());
chooseLeft.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
chooseLeft.setEnabled(false);
chooseRight.setEnabled(true);
upLeft(docRefPosts, chooseLeft);
}
});
chooseRight.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
chooseRight.setEnabled(false);
chooseLeft.setEnabled(true);
upRight(docRefPosts, chooseRight);
}
});
}
Firstly, you should be aware that RecyclerView re-uses its viewholders each time you scroll past the last visible item on the screen.
Based on your implementation for every time a user scroll past the
last visible item or post on the screen, because it is inside a
RecyclerView the original first-item on the list and even other items
being scrolled past are recycled and the next item becomes the new
first-item but being populated with a different post-values as
expected.
But remember your code instantiates a new CountdownTimer for
every new post added to the list (where 50 posts equals 50
Countdowntimers and 50 posts does not necessarily mean 50 views because
some of the views would probably be recycled by the RecyclerView; you get the logic), hence it allows the
possibility for multiple CountdownTimer accessing same view which
causes the flash and will surely lead to a memory leak causing an
OutOfMemoryError when the user keeps scrolling continuously on a long
list.
Recommended Implementation
Create your CountDownTimer inside your ViewHolder Class
public static class PostViewHolder extends RecyclerView.ViewHolder {
...
CountDownTimer countdowntimer;
public FeedViewHolder(View itemView) {
...
}
}
Access the timer inside setPost method
void setPost(final Post post) {
...
long currentTime = date.getTime();
long endTime = post.getTimerEndDate().getTime();
long timeLeft = endTime - currentTime;
if (countdowntimer != null) {
//there is an existing timer so we cancel it to prevent duplicates
countdowntimer.cancel();
}
countdowntimer = new CountDownTimer(timeLeft, 1000) {
#SuppressLint("DefaultLocale")
public void onTick(long millisUntilFinished) {
timer.setText(String.format("%02d:%02d:%02d",
(int) ((millisUntilFinished / (1000 * 60 * 60)) % 24),
(int) ((millisUntilFinished / (1000 * 60)) % 60),
(int) (millisUntilFinished / 1000) % 60));
//here you can have your logic to set text to edittext
}
public void onFinish() {
timer.setText("done!");
}
}.start();
}
Each time you are creating a new timer when your items in the RecyclerView is getting recycled after scrolling. The timeLeft variable also changes each time you scroll the item and that initializes a new Timer which is the reason for the flashing I guess.
Hence I would like to recommend having an array in your adapter. This is for holding all of your CountDownTimers. In the constructor of your adapter, initialize that properly to have the correct values. Let us take a sample implementation like the following.
Declare two arrays like the following.
CountDownTimer[] timers = new CountDownTimer[data.size()];
Now in the constructor of your adapter do something like the following.
// Let us assume the list of Data is dataItems which is being passed to your adapter.
// Your constructor might take other parameters
public MyAdapter(ArrayList<Data> dataItems) {
for(int i = 0; i < dataItems.size(); i++) {
long timeLeft = getTimeLeftValue(dataItems.get(i));
timers[i] = new CountDownTimer(timeLeft, 1000) {
#SuppressLint("DefaultLocale")
public void onTick(long millisUntilFinished) {
timer.setText(String.format("%d:%d:%d",
(int) ((millisUntilFinished / (1000 * 60 * 60)) % 24),
(int) ((millisUntilFinished / (1000 * 60)) % 60),
(int) (millisUntilFinished / 1000) % 60));
//here you can have your logic to set text to edittext
}
public void onFinish() {
timer.setText("done!");
}
}.start();
}
}
private long getTimeLeftValue(Data post) {
Date date = java.util.Calendar.getInstance().getTime();
long currentTime = date.getTime();
long endTime = post.getTimerEndDate().getTime();
timeLeft = endTime - currentTime;
return timeLeft.
}
Now in your onBindViewHolder you need to set the timer as follows.
timerInstance = timers.get(position);
I am assuming that each of your items in the RecyclerView is having a timer instance which is showing the CountDownTimer.
You don't specify where exactly your logic is in your adapter.
The logic should be within onBindViewHolder(…). You can access the widgets using viewHolder.findViewById(...)
Also, CountDownTimer is its own thread, and editing a TextView or any widget can cause an exception. Wrap the timer.setText(…) call within runOnUiThread()
Related
My application should show several timers and start them at the same time.
The problem is that these don't run in sync. You can see this on the GIF, you can see that they lose synchronicity.
After a certain time you can see that a timer is updated faster than the others. First timer 1 is displayed, followed by timer 2 with a delay and then again with a certain delay timer 3.
I want to create multiple timers and each timer has a different duration.
I want to display these timers in a TextView. I initialize this in an ArrayList in the MainActivity. These are then added in a ReyclerView so that they are also displayed. To start, I go through a for loop and execute the startTimer() method for each individual object. Unfortunately, the times are not correct for every timer. That means one timer is faster, the second is slower and so on. So each timer starts at different times or the text changes at different times.
Have only one timing item (e.g., ScheduledExecutorService) in a
viewmodel, not 10 in an activity. While you may want to depict 10
times, in the UI, you only need one timing item to tell you when a
second has elapsed. Have the viewmodel emit details of the time values
that should be rendered in the UI. Have the UI simply display those
time values
This text on top was an answer. How could I build this in so that I only have one timer element and it controls the times? How can I let these timers run synchronously?
Each timer should be updated and displayed at the same time.
I look forward to an answer, thank you in advance for your support!
MainActivity
public class MainActivity extends AppCompatActivity implements ModelTimer.MyCallback {
private RecyclerView recyclerView;
private Button button_start;
private Dialog epicDialog;
public static ArrayList<ModelTimer> timerList;
public static TimerAdapter adapter;
public static Context context;
#Override
protected void onCreate(Bundle savedInstanceState) {
recyclerView = findViewById(R.id.recyclerview_timers);
button_start = findViewById(R.id.button_start);
timerList= new ArrayList<>();
for (int i = 1; i <= 10; i++) {
timerList.add(new Timer(i, 600000 * i))); //Each timer has a different time
}
recyclerView.setLayoutManager(recyclerViewLayoutManager);
adapter = new TimerAdapter(this, timerList);
recyclerView.setAdapter(adapter);
context = this;
button_start.setOnClickListener(v -> {
for (Timer timer: timerList) {
timer.startTimer();
});
#Override
public void updateMyText(int index, long time) {
timerList.get(index-1).setTime(time);
adapter.notifyDataSetChanged();
}
}
#Override
public void updateMyText(int index, long time) {
timerList.get(index-1).setTime(time);
adapter.notifyDataSetChanged();
}
}
ModelTimer
public class ModelTimer {
public interface MyCallback {
public void updateMyText(int index, long time);
}
private int index;
private long time;
private CountDownTimer mCountDownTimer;
private boolean mTimerRunning;
private long startTime;
private long mTimeLeftInMillis;
private String timeLeftFormatted;
private MyCallback myCallback = null;
public ModelTimer(int index, long startTimeMilliseconds, MyCallback callback) {
this.index = index;
this.time = startTimeMilliseconds;
mTimeLeftInMillis = startTimeMilliseconds;
startTime = startTimeMilliseconds;
this.myCallback = callback;
}
public void startTimer() {
mCountDownTimer = new CountDownTimer(mTimeLeftInMillis, 1000) {
#Override
public void onTick(long millisUntilFinished) {
mTimeLeftInMillis = millisUntilFinished;
updateCountDownText();
}
#Override
public void onFinish() {
mTimerRunning = false;
}
}.start();
mTimerRunning = true;
}
public void resetTimer() {
mCountDownTimer.cancel();
mTimerRunning = false;
mTimeLeftInMillis = startTime;
timeLeftFormatted = formattedTime(startTime);
changeText(index-1);
}
public void updateCountDownText() {
//System.out.println("ID: " + getIndex() + " " + mTimeLeftInMillis);
if(myCallback != null) {
myCallback.updateMyText(getIndex(), mTimeLeftInMillis);
}
}
private void changeText(int element) {
String[] data = timeLeftFormatted.split(":");
int hours = Integer.parseInt(data[0]);
int minutes = Integer.parseInt(data[1]);
int seconds = Integer.parseInt(data[2]);
int timeSeconds = seconds + 60 * minutes + 3600 * hours;
long milliseconds = TimeUnit.MILLISECONDS.convert(timeSeconds, TimeUnit.SECONDS);
MainActivity.countdownList.get(element).setTime(milliseconds);
MainActivity.adapter.notifyDataSetChanged();
}
public static long getMilliseconds(String time) {
String[] data = time.split(":");
int hours = Integer.parseInt(data[0]);
int minutes = Integer.parseInt(data[1]);
int seconds = Integer.parseInt(data[2]);
int timeSeconds = seconds + 60 * minutes + 3600 * hours;
return TimeUnit.MILLISECONDS.convert(timeSeconds, TimeUnit.SECONDS);
}
public static String formattedTime(long time) {
int milliToSec = (int) (time / 1000);
int hours = milliToSec / 3600;
int minutes = (milliToSec / 60) % 60;
int seconds = milliToSec % 60;
return String.format(Locale.getDefault(), "%02d:%02d:%02d", hours, minutes, seconds);
}
public int getIndex() {
return this.index;
}
public void setIndex(int index) {
this.index = index;
}
public long getTime() { return this.time; }
public void setTime(long time) {
this.time = time;
}
}
TimerAdapter
public class TimerAdapter extends RecyclerView.Adapter<TimerAdapter.ViewHolder> {
private Context mContext;
private ArrayList<ModelTimer> list;
static String indexCountdown;
TimerAdapter(Context context, ArrayList<ModelTimer> list) {
mContext = context;
this.list = list;
}
#NonNull
#Override
public ViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
View view = layoutInflater.inflate(R.layout.recyclerview_countdowns, parent, false);
ViewHolder viewHolder = new ViewHolder(view);
return viewHolder;
}
#Override
public void onBindViewHolder(#NonNull ViewHolder holder, int position) {
ModelTimer timerItemList = list.get(position);
TextView index = holder.countdown_index;
TextView time = holder.countdown_time;
CardView layout = holder.layout_countdowns;
index.setText(timerItemList .getIndex() +"");
time.setText(ModelTimer.formattedTime(timerItemList .getTime()));
}
#Override
public int getItemCount() {
return list.size();
}
public class ViewHolder extends RecyclerView.ViewHolder {
TextView countdown_index, countdown_time;
private CardView layout_countdowns;
public ViewHolder(#NonNull View itemView) {
super(itemView);
countdown_index = itemView.findViewById(R.id.coutdown_index);
countdown_time = itemView.findViewById(R.id.countdown_time);
layout_countdowns = itemView.findViewById(R.id.layout_countdown);
}
}
}
You can try it with CountDownTimer if you want. It would look like this:
public class TimerHelper {
public interface UpdateCallback {
/**
* #return true if want to be updated again false otherwise
*/
boolean secondPassedCallback();
}
private ArrayList<UpdateCallback> callbacksList = new ArrayList<>();
/**
* #param maxTime maximum of all timers
*/
public void start(int maxTime) {
CountDownTimer mCountDownTimer = new CountDownTimer(maxTime, 1000) {
#Override
public void onTick(long millisUntilFinished) {
ArrayList<UpdateCallback> itemsToRemove=new ArrayList<>();
for (UpdateCallback updateCallback : callbacksList) {
boolean wantToBeUpdated = updateCallback.secondPassedCallback(); // remove timers that reached 0
if (!wantToBeUpdated) {
itemsToRemove.add(updateCallback);
}
}
callbacksList.removeAll(itemsToRemove);
}
#Override
public void onFinish() {
}
};
mCountDownTimer.start();
}
public boolean addCallback(UpdateCallback callback) {
return callbacksList.add(callback);
}
public boolean removeCallback(UpdateCallback callback) {
return callbacksList.remove(callback);
}
}
With this just make your ModelTimer implement this callback and do the UI update there. Finally in activity change button_start OnClickListener to:
button_start.setOnClickListener(v -> {
TimerHelper t = new TimerHelper();
for (ModelTimer timer : timerList) {
t.addCallback(timer);
}
});
You can in your ReyclerView ViewHolder instead of inside in ModelTimer
chronometer.setBase(new Date().getTime());
chronometer.setOnChronometerTickListener(new OnChronometerTickListener() {
public void onChronometerTick(Chronometer cArg) {
long t = SystemClock.elapsedRealtime() - cArg.getBase();
cArg.setText(DateFormat.format("HH:mm:ss", t));
}
});
chronometer.start();
Check updates below.
I am making an app for a stair climbing challenge that tracks the date and number of steps taken (user input, not automatic). I have an ArrayList that stored objects containing the following three variables:
String date
Int steps
Instant timeStamp
The app has two input buttons, one for integer step input, and one for date selection. There is a simple method created to filter the visible list by the selected date and a couple of visual indicators of your daily progress vs. the daily goal for flights of stairs for the day.
App screenshot
I am using the Instant variable as a timestamp to try to get around the issue of the OnClickListener selecting the position of the item from the filtered list instead of the corresponding item in the unfiltered list. I do this by using the position reported from the OnClickListener to fetch the timeStamp variable from the associated item in the filtered ArrayList, then compare that timeStamp to the items in the unfiltered ArrayList and fetch the indexOf the matching item.
All filtered ArrayLists show properly in the RecyclerView when you select a date.
The problem comes in removing items. If I add items only to one date, then you can remove and add items as you'd expect.
App function without date change (gif)
If I add to one date, then another, while they display properly, the items will be removed from the correct position but in the date you first added items, regardless of whether that is the currently selected date or not.
App function with changing date (gif)
I feel like I'm missing something relatively simple here and my brain is just too saturated with this project to see it.
Main Activity:
public class MainActivity extends AppCompatActivity {
private RecyclerView mRecyclerView;
private ExampleAdapter mAdapter;
private RecyclerView.LayoutManager mLayoutManager;
Date temp_curr_date = Calendar.getInstance().getTime();
SimpleDateFormat df = new SimpleDateFormat("dd-MMM-yyyy");
String sel_date = df.format(temp_curr_date);
String curr_date = df.format(temp_curr_date);
double daily_total;
int progress = 0;
double daily_goal = 7.5;
TextView textView1;
TextView textView2;
TextView textViewFlights;
ProgressBar pb;
List<ExampleItem> mExampleList;
List<ExampleItem> filteredList;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// ----- LOAD SAVED ARRAY LIST -----
loadData();
// ----- SET VARIABLES -----
daily_total = totalOutput(mExampleList, sel_date);
textView1 = findViewById(R.id.total);
textView1.setText(String.valueOf(daily_total));
textViewFlights = findViewById(R.id.flights);
pb = findViewById(R.id.progress_bar);
pb.setProgress(getProgress(mExampleList, sel_date), true);
// ----- BUILD RECYCLERVIEW -----
buildRecyclerView();
filter(sel_date);
// ----- ADD STEPS DIALOGUE -----
setAddStepButton();
// ----- CALENDAR DIALOGUE -----
setDateChangeButton();
}
public double totalOutput(List<ExampleItem> steps, String date) {
try{
int temp_total = 0;
double flight_total;
for (int a = 0; a < steps.size(); a++) {
if (date.equals(steps.get(a).getText1()))
temp_total += steps.get(a).getText2();
}
flight_total = round(temp_total / 16.0, 2);
return flight_total;
} catch (Exception e){
return 0.0;
}
}
public static double round(double value, int places) {
if (places < 0) throw new IllegalArgumentException();
BigDecimal bd = new BigDecimal(value);
bd = bd.setScale(places, RoundingMode.HALF_UP);
return bd.doubleValue();
}
public static int toInt(double value) {
BigDecimal bd = new BigDecimal(value);
bd = bd.setScale(0, RoundingMode.HALF_UP);
return bd.intValue();
}
public static Date getDate(int year, int month, int day) {
Calendar cal = Calendar.getInstance();
cal.set(Calendar.YEAR, year);
cal.set(Calendar.MONTH, month);
cal.set(Calendar.DAY_OF_MONTH, day);
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
return cal.getTime();
}
private void saveData(){
SharedPreferences sharedPreferences = getSharedPreferences("shared preferences", MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
Gson gson = new Gson();
String json = gson.toJson(mExampleList);
editor.putString("task list", json);
editor.apply();
}
private void loadData(){
SharedPreferences sharedPreferences = getSharedPreferences("shared preferences", MODE_PRIVATE);
Gson gson = new Gson();
String json = sharedPreferences.getString("task list", null);
Type type = new TypeToken<ArrayList<ExampleItem>>() {}.getType();
mExampleList = gson.fromJson(json, type);
if (mExampleList == null){
mExampleList = new ArrayList<>();
}
}
private int getProgress(List<ExampleItem> steps, String date){
int daily_progress_int;
try{
int temp_progress = 0;
double flight_total;
for (int a = 0; a < steps.size(); a++) {
if (date.compareTo(steps.get(a).getText1()) == 0)
temp_progress += steps.get(a).getText2();
}
flight_total = round(temp_progress / 16.0, 2);
daily_progress_int = toInt((flight_total/daily_goal)*100);
return daily_progress_int;
} catch (Exception e){
return 0;
}
}
private void addProgress(double x, int prog){
int daily_progress_int = toInt((x/daily_goal)*100);
if (progress <= 100-daily_progress_int){
progress = progress + prog;
pb = findViewById(R.id.progress_bar);
pb.setProgress(daily_progress_int, true);
} else if (progress + daily_progress_int > 100){
pb = findViewById(R.id.progress_bar);
pb.setProgress(100, true);
}
}
private void removeProgress(double x, int prog){
int daily_progress_int = toInt((x/daily_goal)*100);
progress = progress - prog;
if (progress <= 100) {
pb = findViewById(R.id.progress_bar);
pb.setProgress(daily_progress_int, true);
} else {
pb = findViewById(R.id.progress_bar);
pb.setProgress(0, true);
}
}
public void addItem(String date, int steps, Instant ts){
mExampleList.add(new ExampleItem(date, steps, ts));
filter(sel_date);
}
public void removeItem(final int position){
final AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
View viewInflated = LayoutInflater.from(MainActivity.this).inflate(R.layout.confirm, (ViewGroup) findViewById(android.R.id.content), false);
builder.setCancelable(true);
builder.setView(viewInflated);
builder.setPositiveButton("Yup",
new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int which) {
mExampleList.remove(position);
mAdapter.notifyDataSetChanged(position);
filter(sel_date);
daily_total = totalOutput(mExampleList, sel_date);
textView1 = findViewById(R.id.total);
textView1.setText(String.valueOf(daily_total));
removeProgress(daily_total,progress);
if (daily_total == 1.0){
textViewFlights.setText("flight");
} else {
textViewFlights.setText("flights");
}
saveData();
}
});
builder.setNegativeButton("Nope", new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int which) {
}
});
AlertDialog dialog = builder.create();
dialog.show();
}
public void buildRecyclerView(){
mRecyclerView = findViewById(R.id.recyclerView);
mRecyclerView.setHasFixedSize(true);
mLayoutManager = new LinearLayoutManager(this);
mAdapter = new ExampleAdapter(mExampleList);
mRecyclerView.setLayoutManager(mLayoutManager);
mRecyclerView.setAdapter(mAdapter);
mAdapter.setOnItemClickListener(new ExampleAdapter.OnItemClickListener() {
#Override
public void onItemClick(int position) {
Instant test = filteredList.get(position).getTimeStamp();
for (ExampleItem item : mExampleList){
if (test.compareTo(item.getTimeStamp()) == 0){
removeItem(mExampleList.indexOf(item));
}
});
}
public void filter(String text){
filteredList = new ArrayList<>();
for (ExampleItem item : mExampleList){
if (item.getText1().toLowerCase().contains(text.toLowerCase())){
filteredList.add(item);
}
}
mAdapter.filterList(filteredList);
}
public void setAddStepButton(){
FloatingActionButton fab = findViewById(R.id.addSteps);
fab.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
View viewInflated = LayoutInflater.from(MainActivity.this).inflate(R.layout.add_steps, (ViewGroup) findViewById(android.R.id.content), false);
// Step input
final EditText input = viewInflated.findViewById(R.id.input);
builder.setView(viewInflated);
// OK Button
builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int which) {
if (input.getText().length() != 0) {
try {
int in = Integer.parseInt(String.valueOf(input.getText()));
if (in > 0) {
Instant timeStamp = Instant.now();
addItem(sel_date, in, timeStamp);
dialog.dismiss();
} else {
dialog.cancel();
}
} catch (Exception e) {
dialog.cancel();
}
daily_total = totalOutput(mExampleList, sel_date);
textView1 = findViewById(R.id.total);
textView1.setText(String.valueOf(daily_total));
addProgress(daily_total, progress);
mAdapter.notifyDataSetChanged();
filter(sel_date);
if (daily_total == 1.0){
textViewFlights.setText("flight");
} else {
textViewFlights.setText("flights");
}
saveData();
} else{
dialog.cancel();
}
}
});
// Cancel Button
builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
}
});
builder.show();
}
});
}
public void setDateChangeButton(){
FloatingActionButton fabcal = findViewById(R.id.calendarButton);
fabcal.setOnClickListener(new View.OnClickListener(){
#Override
public void onClick(View view) {
LayoutInflater inflater = (LayoutInflater)getApplicationContext().getSystemService
(Context.LAYOUT_INFLATER_SERVICE);
LinearLayout ll= (LinearLayout)inflater.inflate(R.layout.calendar, null, false);
CalendarView cv = (CalendarView) ll.getChildAt(0);
long milliseconds = 0;
try {
Date d = df.parse(sel_date);
milliseconds = d.getTime();
} catch (ParseException e) {
e.printStackTrace();
}
cv.setDate(milliseconds);
cv.setOnDateChangeListener(new CalendarView.OnDateChangeListener() {
#Override
public void onSelectedDayChange(
#NonNull CalendarView view,
int year,
int month,
int dayOfMonth)
{
Date temp_sel_date = getDate(year, month, dayOfMonth);
sel_date = df.format(temp_sel_date);
textView2 = findViewById(R.id.daily_total);
if (sel_date.equals(curr_date)){
textView2.setText("Today");
} else {
String dt_day = (String) DateFormat.format("dd", temp_sel_date);
String dt_month = (String) DateFormat.format("MMM", temp_sel_date);
textView2.setText(dt_month + " " + dt_day);
}
daily_total = totalOutput(mExampleList, sel_date);
textView1 = findViewById(R.id.total);
textView1.setText(String.valueOf(daily_total));
pb = findViewById(R.id.progress_bar);
pb.setProgress(getProgress(mExampleList, sel_date), true);
mAdapter.notifyDataSetChanged();
filter(sel_date);
}
});
new AlertDialog.Builder(MainActivity.this)
.setView(ll)
.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
dialog.dismiss();
}
}
).show();
}
});
}
}
Adapter Class:
public class ExampleAdapter extends RecyclerView.Adapter<ExampleAdapter.ExampleViewHolder> {
private static List<ExampleItem> mExampleList;
private static List<ExampleItem> exampleListFull;
private OnItemClickListener mListener;
public interface OnItemClickListener{
void onItemClick(int position);
}
public void setOnItemClickListener(OnItemClickListener listener){
mListener = listener;
}
public static class ExampleViewHolder extends RecyclerView.ViewHolder {
public TextView mTextView1;
public ImageView mDeleteImage;
public ExampleViewHolder(View itemView, final OnItemClickListener listener) {
super(itemView);
mTextView1 = itemView.findViewById(R.id.textView);
mDeleteImage = itemView.findViewById(R.id.image_delete);
mDeleteImage.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
if (listener != null){
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION){
Instant test = mExampleList.get(position).getTimeStamp();
for (ExampleItem item : exampleListFull){
int compare = test.compareTo(item.getTimeStamp());
if (compare == 0){
int delIndex = exampleListFull.indexOf(item);
position = delIndex;
}
}
listener.onItemClick(position);
}
}
}
});
}
}
public ExampleAdapter(List<ExampleItem> exampleList){
this.mExampleList = exampleList;
exampleListFull = new ArrayList<>(exampleList);
}
#NonNull
#Override
public ExampleViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int i) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.example_item, parent, false);
ExampleViewHolder evh = new ExampleViewHolder(v, mListener);
return evh;
}
#Override
public void onBindViewHolder(#NonNull ExampleViewHolder holder, int position) {
ExampleItem currentItem = mExampleList.get(position);
if (currentItem.getText2() == 1.0){
holder.mTextView1.setText(currentItem.getText2() + " step");
} else {
holder.mTextView1.setText(currentItem.getText2() + " steps");
}
}
#Override
public int getItemCount() {
return mExampleList.size();
}
public void filterList(List<ExampleItem> filteredList){
mExampleList = filteredList;
notifyDataSetChanged();
}
}
If anyone out there has any ideas, I'd love to hear from you!
UPDATE: The included code now reflects the changes suggested by users and is fully functional.
You should use
mAdapter.notifyDataSetChanged();
Instead of
mAdapter.notifyItemRemoved(position);
For more details Visit
this.
I figured it out, for those interested. Comparing the timestamps was working fine, but I put it in the wrong part of the OnClickListener cycle. It needed to be placed in the MainActivity buildRecyclerView method in the setOnClickListener override. I've updated original code to reflect this change.
Thank you anyone that took the time to look at my post.
maybe it could be better to place the onClickListner in Your adpater-class..so You can control every single card if it should be clickable or not...or if You want to place any Ads between the cards
GGK
I'm trying to see what is making my listview jerk sometimes when scroll, at times it's bad especially when the application first launches.
All the conditions I have are necessary, unless there is something I don't know(highly likely).
I'm not running certain tasks on a seperate thread because they are dependent on the data I receive from the backend(I'm coding both, so backend suggestions are welcome as well). Product is in beta but really need to make this a slightly bit smoother. I'm compressing the images, and they are a bit long but it's not the problem because when I upload the images from the device, I also include the width and height of the image and send that along to the backend. These dimensions come back when loading the list.
One thing I wonder is if calculating/converting the dimensions for the specific device's screen is causing the slight lag. Not sure how resource intensive that task is, but without it(without knowing the dimensions, each row would start out flat and then expand to the actual picture size which would cause the list to jump, so I can't run that calculation on the background either.)
Basically the scrolling isn't bad, but I need to improve this somehow.
Here is my Adapter:
public class VListAdapter extends BaseAdapter {
ViewHolder viewHolder;
private boolean isItFromProfile;
/**
* fields For number formating, ex. 1000
* would return 1k in the format method
*/
private static final NavigableMap<Long, String> suffixes = new TreeMap<>();
static {
suffixes.put(1_000L, "k");
suffixes.put(1_000_000L, "M");
suffixes.put(1_000_000_000L, "G");
suffixes.put(1_000_000_000_000L, "T");
suffixes.put(1_000_000_000_000_000L, "P");
suffixes.put(1_000_000_000_000_000_000L, "E");
}
private Context mContext;
private LayoutInflater mInflater;
private ArrayList<Post> mDataSource;
private static double lat;
private static double lon;
public VListAdapter(Context context, ArrayList<Post> items) {
mContext = context;
mDataSource = items;
//mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
isItFromProfile = false;
mInflater = LayoutInflater.from(context);
}
public VListAdapter() {
}
public VListAdapter(Context baseContext, ArrayList<Post> posts, boolean b) {
mContext = baseContext;
mDataSource = posts;
//mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
isItFromProfile = b;
mInflater = LayoutInflater.from(baseContext);
}
public void addElement(Post post) {
mDataSource.add(0, post);
this.notifyDataSetChanged();
}
#Override
public int getCount() {
return mDataSource.size();
}
#Override
public Object getItem(int position) {
return mDataSource.get(position);
}
#Override
public long getItemId(int position) {
return position;
}
#Override
public View getView(final int position, View convertView, ViewGroup parent) {
int limit = Math.min(position + 4, getCount());
for (int i = position; i < limit; i++) {
Glide.with(mContext).load(((Post) getItem(i)).getFilename().toString()).preload();
}
// StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectDiskReads()
// .detectDiskWrites().detectNetwork()
// .penaltyLog().build());
View rowView = convertView;
if (rowView == null) {
viewHolder = new ViewHolder();
rowView = mInflater.inflate(R.layout.mylist, parent, false);
viewHolder.titleTextView = (TextView) rowView.findViewById(R.id.usernameinlist);
viewHolder.timeago = (TextView) rowView.findViewById(R.id.timeago);
//viewHolder.sharebutton = (ImageView) rowView.findViewById(R.id.sharebutton);
viewHolder.likesTextView = (TextView) rowView.findViewById(R.id.likestext);
viewHolder.viewcount = (TextView) rowView.findViewById(R.id.viewcount);
viewHolder.distance = (TextView) rowView.findViewById(R.id.distance);
viewHolder.footprints = (TextView) rowView.findViewById(R.id.footprintcount);
viewHolder.postText = (TextView) rowView.findViewById(R.id.posttext);
viewHolder.profilePic = (ImageView) rowView.findViewById(R.id.profilethumb);
viewHolder.caption = (TextView) rowView.findViewById(R.id.captiontext);
viewHolder.moremenu = (ImageView) rowView.findViewById(R.id.dots);
viewHolder.likesPic = (ImageView) rowView.findViewById(R.id.likeimage);
viewHolder.mapitPic = (ImageView) rowView.findViewById(R.id.mapimage);
viewHolder.playbutton = (ImageView) rowView.findViewById(R.id.playbutton);
viewHolder.videoThumb = (ImageView) rowView.findViewById(R.id.videothumb);
viewHolder.listphoto = (ImageView) rowView.findViewById(R.id.listphoto);
viewHolder.rainbow = (ImageView) rowView.findViewById(R.id.rainbow);
rowView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) rowView.getTag();
}
final Post post = (Post) getItem(position);
int color = Color.parseColor("#dddddd");
viewHolder.likesPic.setColorFilter(color);
viewHolder.mapitPic.setColorFilter(color);
viewHolder.moremenu.setColorFilter(color);
if (Hawk.count() == 0)
initHawkWithDataFromServer();
if (isItFromProfile) {
viewHolder.profilePic.setVisibility(View.GONE);
viewHolder.titleTextView.setVisibility(View.GONE);
viewHolder.distance.setVisibility(View.GONE);
}
viewHolder.titleTextView.setText(post.getUsername());
PrettyTime prettyTime = new PrettyTime();
DateTime dateTime = new DateTime(post.getUploadDate().get$date());
viewHolder.timeago.setText(prettyTime.format(dateTime.toDate()));
viewHolder.likesTextView.setText(String.valueOf(format(post.getLikes())));
viewHolder.footprints.setText(String.valueOf(format(post.getLocation().size() - 1)));
//don't display 0 if there are no likes, just show heart icon
if (viewHolder.likesTextView.getText().equals("0"))
viewHolder.likesTextView.setVisibility(View.GONE);
else
viewHolder.likesTextView.setVisibility(View.VISIBLE);
//don't display 0 if there are no footprints
if (viewHolder.footprints.getText().equals("0"))
viewHolder.footprints.setVisibility(View.GONE);
else
viewHolder.footprints.setVisibility(View.VISIBLE);
double[] loc = post.getLocation().get(0);
viewHolder.distance.setText("~" + PostListFragment.distance(loc[0], loc[1], 'M') + " Miles");
if (post.getViews() != null)
viewHolder.viewcount.setText(format(post.getViews()) + (post.getViews() == 1 ? " View" : " Views"));
String profilePictureS3Url = "https://s3-us-west-2.amazonaws.com/moleheadphotos/" + post.getUsername()
+ ".jpg";
String filename = post.getS3link();
final String videoThumbURL = "https://s3-us-west-2.amazonaws.com/moleheadphotos/" + filename;
Glide.with(mContext).load(profilePictureS3Url).asBitmap().centerCrop().into(new BitmapImageViewTarget(viewHolder.profilePic) {
#Override
protected void setResource(Bitmap resource) {
RoundedBitmapDrawable circularBitmapDrawable =
RoundedBitmapDrawableFactory.create(mContext.getResources(), resource);
circularBitmapDrawable.setCircular(true);
viewHolder.profilePic.setImageDrawable(circularBitmapDrawable);
}
});
int height = ((Post) getItem(position)).getHeight();
int width = ((Post) getItem(position)).getWidth();
if (height != 0 && width != 0) {
ViewGroup.LayoutParams params = viewHolder.listphoto.getLayoutParams();
Resources r = mContext.getResources();
height = (int) getHeight(height, width);
params.height = height;
params.width = ViewGroup.LayoutParams.MATCH_PARENT;
viewHolder.listphoto.setLayoutParams(params);
} else {
ViewGroup.LayoutParams params = viewHolder.listphoto.getLayoutParams();
params.height = ViewGroup.LayoutParams.WRAP_CONTENT;
params.width = ViewGroup.LayoutParams.MATCH_PARENT;
viewHolder.listphoto.setLayoutParams(params);
}
if (post.getType() == null) {
Glide.clear(viewHolder.listphoto);
viewHolder.listphoto.setVisibility(View.GONE);
//Glide.clear(viewHolder.listphoto);
viewHolder.videoThumb.setVisibility(View.VISIBLE);
viewHolder.rainbow.setVisibility(View.VISIBLE);
Glide.with(mContext).load(videoThumbURL).fitCenter()
.diskCacheStrategy(DiskCacheStrategy.ALL).dontAnimate().into(viewHolder.videoThumb);
viewHolder.playbutton.setVisibility(View.VISIBLE);
}
if (post.getType() != null) {
if (post.getType().equals("video")) {
viewHolder.playbutton.setVisibility(View.VISIBLE);
Glide.clear(viewHolder.listphoto);
viewHolder.listphoto.setVisibility(View.GONE);
Glide.clear(viewHolder.postText);
viewHolder.postText.setVisibility(View.GONE);
viewHolder.videoThumb.setVisibility(View.VISIBLE);
viewHolder.rainbow.setVisibility(View.VISIBLE);
Glide.with(mContext).load(videoThumbURL).fitCenter()
.diskCacheStrategy(DiskCacheStrategy.ALL).dontAnimate().into(viewHolder.videoThumb);
}
if (post.getType().equals("image")) {
Glide.clear(viewHolder.videoThumb);
viewHolder.videoThumb.setVisibility(View.GONE);
viewHolder.rainbow.setVisibility(View.GONE);
Glide.clear(viewHolder.playbutton);
viewHolder.playbutton.setVisibility(View.GONE);
Glide.clear(viewHolder.postText);
viewHolder.postText.setVisibility(View.GONE);
viewHolder.listphoto.setVisibility(View.VISIBLE);
viewHolder.listphoto.setBottom(0);
Glide.with(mContext).load(post.getFilename().toString())
.diskCacheStrategy(DiskCacheStrategy.ALL).dontAnimate()
.into(viewHolder.listphoto);
}
if (post.getType().equals("text")) {
Glide.clear(viewHolder.videoThumb);
viewHolder.videoThumb.setVisibility(View.GONE);
viewHolder.rainbow.setVisibility(View.GONE);
Glide.clear(viewHolder.playbutton);
viewHolder.playbutton.setVisibility(View.GONE);
Glide.clear(viewHolder.listphoto);
viewHolder.listphoto.setVisibility(View.GONE);
viewHolder.postText.setVisibility(View.VISIBLE);
viewHolder.postText.setText(post.getText());
}
}
if (Hawk.contains("liked" + post.getId().get$oid())) {
viewHolder.likesPic.clearColorFilter();
Glide.with(mContext).load(R.drawable.heartroundorange).into(viewHolder.likesPic);
((ImageView) viewHolder.likesPic).setColorFilter(Color.parseColor("#ff3a6f"));
} else {
Glide.with(mContext).load(R.drawable.heartroundgray).diskCacheStrategy(DiskCacheStrategy.ALL)
.into(viewHolder.likesPic);
}
if (Hawk.contains("mapped" + post.getId().get$oid())) {
viewHolder.mapitPic.clearColorFilter();
((ImageView) viewHolder.mapitPic).setImageResource(R.drawable.dropmaincolororange);
((ImageView) viewHolder.mapitPic).setColorFilter(Color.parseColor("#444444"));
} else {
Glide.with(mContext).load(R.drawable.dropdarkgray).diskCacheStrategy(DiskCacheStrategy.ALL)
.into(viewHolder.mapitPic);
}
if (!Hawk.contains("mapped" + post.getId().get$oid())) {
viewHolder.mapitPic.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
Hawk.put("mapped" + post.getId().get$oid(), 1);
((ImageView) viewHolder.mapitPic).setImageResource(R.drawable.dropmaincolororange);
viewHolder.footprints.setText(String.valueOf(post.getLocation().size() + 1));
post.getLocation().add(new double[]{PostListFragment.lon, PostListFragment.lat});
notifyDataSetChanged();
Thread t = new Thread(new Runnable() {
#Override
public void run() {
postMappedToServer(post.getId().get$oid());
}
});
t.start();
TastyToast.makeText(mContext, "Post dropped off here.", TastyToast.LENGTH_SHORT, TastyToast.CONFUSING);
}
});
} else {
viewHolder.mapitPic.setClickable(false);
}
if (!Hawk.contains("liked" + post.getId().get$oid())) {
viewHolder.likesPic.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
Hawk.put("liked" + post.getId().get$oid(), 1);
viewHolder.likesPic.setClickable(false);
((ImageView) viewHolder.likesPic).setImageResource(R.drawable.heartroundorange);
viewHolder.likesTextView.setText(String.valueOf(post.getLikes() + 1));
post.setLikes(post.getLikes() + 1);
notifyDataSetChanged();
Thread t = new Thread(new Runnable() {
#Override
public void run() {
postLikeToServer(post);
}
});
t.start();
}
});
} else {
viewHolder.likesPic.setClickable(false);
}
if (post.getType() == null || post.getType().equals("video"))
viewHolder.videoThumb.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
if (VListAdapter.this.mContext instanceof ProfileFeed) {
((ProfileFeed) VListAdapter.this.mContext).closeActivity();
}
Intent broadcast = new Intent();
broadcast.setAction("com.molehead.openout.POST");
broadcast.putExtra("postId", post.getFilename().toString());
broadcast.putExtra("hawkId", post.getId().get$oid());
broadcast.putExtra("s3link", post.getS3link());
broadcast.putExtra("username", post.getUsername());
if (Hawk.contains("liked" + post.getId().get$oid()))
broadcast.putExtra("liked", "yes");
else
broadcast.putExtra("liked", "no");
broadcast.putExtra("likecount", post.getLikes().toString());
App.post = post;
LocalBroadcastManager.getInstance(mContext.getApplicationContext()).sendBroadcast(broadcast);
}
});
viewHolder.moremenu.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
PopupMenu popup = new PopupMenu(mContext.getApplicationContext(), viewHolder.moremenu, Gravity.CENTER);
//Inflating the Popup using xml file
popup.getMenuInflater().inflate(R.menu.menu_main, popup.getMenu());
//registering popup with OnMenuItemClickListener
popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_share:
String postId = post.getId().get$oid();
Intent sharingIntent = new Intent(android.content.Intent.ACTION_SEND);
sharingIntent.setType("text/plain");
String shareBody = postId + ".jpg"; //https://openout.herokuapp.com/posts/" + postId;
String shareSub = "Shared via Molehead";
sharingIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, shareSub);
sharingIntent.putExtra(android.content.Intent.EXTRA_TEXT, shareBody);
sharingIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Intent new_intent = Intent.createChooser(sharingIntent, "Share");
new_intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.getApplicationContext().startActivity(new_intent);
break;
}
return true;
}
});
popup.show();
}
});
return rowView;
}
private void initHawkWithDataFromServer() {
SharedPreferences settings = mContext.getApplicationContext().getSharedPreferences("userinfo", 0);
String username = settings.getString("username", "ok");
String password = settings.getString("password", "ok");
LoginService loginService =
ServiceGenerator.createService(LoginService.class, username, password);
final Call<List<Post>> call = loginService.getLikes(username);
Log.i("lonlat", String.valueOf(lon) + " and " + String.valueOf(lat));
call.enqueue(new Callback<List<Post>>() {
#Override
public void onResponse(Call<List<Post>> call, Response<List<Post>> response) {
ArrayList<Post> posts = new ArrayList<>();
posts = (ArrayList<Post>) response.body();
if (!posts.isEmpty())
for (Post p : posts) {
Hawk.put("liked" + p.getId().get$oid(), 1);
}
}
#Override
public void onFailure(Call<List<Post>> call, Throwable t) {
}
});
}
private void postMappedToServer(String oid) {
SharedPreferences settings = mContext.getSharedPreferences("userinfo", 0);
String username = settings.getString("username", "ok");
String password = settings.getString("password", "ok");
LoginService loginService =
ServiceGenerator.createService(LoginService.class, username, password);
Log.i("postlistfraglat", String.valueOf(PostListFragment.lat));
Call<ResponseBody> call = loginService.addLocation(oid, PostListFragment.lon, PostListFragment.lat);
call.enqueue(new Callback<ResponseBody>() {
#Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
if (response.isSuccessful())
Log.i("mapped", "success");
}
#Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
}
});
}
public void postLikeToServer(Post post) {
SharedPreferences settings = mContext.getSharedPreferences("userinfo", 0);
String username = settings.getString("username", "ok");
String password = settings.getString("password", "ok");
LoginService loginService =
ServiceGenerator.createService(LoginService.class, username, password);
Call<ResponseBody> call = loginService.like(post, 1, username);
call.enqueue(new Callback<ResponseBody>() {
#Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
if (response.isSuccessful()) {
try {
Log.i("call", response.body().string());
} catch (IOException e) {
e.printStackTrace();
}
}
}
#Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
Log.i("MFEED", "like request failed");
}
});
}
public static String format(long value) {
//Long.MIN_VALUE == -Long.MIN_VALUE so we need an adjustment here
if (value == Long.MIN_VALUE) return format(Long.MIN_VALUE + 1);
if (value < 0) return "-" + format(-value);
if (value < 1000) return Long.toString(value); //deal with easy case
Map.Entry<Long, String> e = suffixes.floorEntry(value);
Long divideBy = e.getKey();
String suffix = e.getValue();
long truncated = value / (divideBy / 10); //the number part of the output times 10
boolean hasDecimal = truncated < 100 && (truncated / 10d) != (truncated / 10);
return hasDecimal ? (truncated / 10d) + suffix : (truncated / 10) + suffix;
}
static class ViewHolder {
private TextView titleTextView;
private TextView timeago;
private TextView likesTextView;
private TextView viewcount;
private TextView distance;
private TextView footprints;
private ImageView profilePic;
private ImageView moremenu;
private ImageView likesPic;
private ImageView mapitPic;
private ImageView rainbow;
//private ImageView sharebutton;
private TextView caption;
private ImageView listphoto;
private ImageView videoThumb;
private ImageView playbutton;
private TextView postText;
private Post post;
}
private float getHeight(float height, float width) {
WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
Point size = new Point();
display.getSize(size);
return (height * size.x / width);
}
}
It is impossible to point to a specific issue because there is so much code in your Adapter. One thing is sure, though - switching to RecyclerView won't help you in this case.
Adapters should not contain business logic - they should only "adapt" input objects to the underlying Views. In your case, it seems like the adapter performs calculations, spawns new threads, performs network requests, etc.
You need to refactor your code such that the adapter will be similar to this:
public class PostsListAdapter extends ArrayAdapter<Post> {
private Context mContext;
public PostsListAdapter(Context context, int resource) {
super(context, resource);
mContext = context;
}
public void bindPosts(List<Post> posts) {
clear();
addAll(posts);
notifyDataSetChanged();
}
#NonNull
#Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
// assign new View to convertView
// create new ViewHolder
// set ViewHolder as tag of convertView
// set listeners
} else {
// get a reference to existing ViewHolder
}
// populate ViewHolder's elements with data from getItem(position)
// kick off asynchronous loading of images
// NOTE: no calculations allowed here - just simple bidding of data to Views
return convertView;
}
}
Your code needs to be structured in such a way, that business logic that involves calculations and transformation of data executed before you bind a new data to ListView, and Post objects that you pass to bindPosts() method already contain the results of the aforementioned calculations and transformations.
Adapter just "adapts" the final data from Posts to Views - nothing more.
If you're short on time now, and just need to "make it work", then I would start by removing the logic that spawns new threads and makes network requests. See if this improves performance.
Change your implementation to RecyclerView which is more efficient in terms of scrapping views or recycling.
We can also enable optimizations if the items are static and will not change for significantly smoother scrolling:
recyclerView.setHasFixedSize(true);
Create an intent service and register BroadcastReceiver as data return callback or error callback when api request, business rule, data modification completed. Use synchronous call to execute initHawkWithDataFromServer() in advance and after getting result from api continue modifying or applying business logic. After that create new adapter or update existing adapter data set.
Move all the below data calculation or data value formatting logic from adapter's getView() to above intent service.
You can add more getter and setter to existing Post pojo.
DateTime dateTime = new DateTime(post.getUploadDate().get$date());
viewHolder.timeago.setText(prettyTime.format(dateTime.toDate()));
viewHolder.likesTextView.setText(String.valueOf(format(post.getLikes())));
viewHolder.footprints.setText(String.valueOf(format(post.getLocation().size)) - 1)));
Post{
//Your existing property
#Expose(serialize = false, deserialize = false)
//equals neither serialize nor deserialize or
private DateTime uploadedDateTime;
//etc. prettyTime.format, String.valueOf
}
Removes unnecessary reflection:
GsonBuilder builder = new GsonBuilder();
builder.excludeFieldsWithoutExposeAnnotation();
Gson gson = builder.create();
new Retrofit.Builder().addConverterFactory(GsonConverterFactory.create(gson)).build();
and it to your retrofit Service creation class.
You can also use transient(private transient DateTime uploadedDateTime;)
Remove
public void addElement(Post post) { mDataSource.add(0, post);
this.notifyDataSetChanged();}
and whenever you need to notify if a single or more items inserted, deleted etc. Use the below:
notifyItemChanged(int)
notifyItemInserted(int)
notifyItemRemoved(int)
notifyItemRangeChanged(int, int)
notifyItemRangeInserted(int, int)
notifyItemRangeRemoved(int, int)
We can use these from the activity or fragment:
//Add a new contact
items.add(0, new Post("Barney"));
//Notify the adapter that an item was inserted at position 0
adapter.notifyItemInserted(0);
Above methods are more efficient. Every time we want to add or remove items from the RecyclerView, we will need to explicitly inform to the adapter of the event. Unlike the ListView adapter, a RecyclerView adapter should not rely on notifyDataSetChanged() since the more granular actions should be used. See the API documentation for more details
Also, if you are intending to update an existing list, make sure to get the current count of items before making any changes. For instance, a getItemCount() on the adapter should be called to record the first index that will be changed.
// record this value before making any changes to the existing list
int curSize = adapter.getItemCount(); // replace this line with wherever you get new records
ArrayList<Post> newItems = Post.createPostsList(20);
// update the existing list
items.addAll(newItems);
// curSize should represent the first element that got added
// newItems.size() represents the itemCount
adapter.notifyItemRangeInserted(curSize, newItems.size());
Diffing Larger Changes
A new DiffUtil class has been added in the v24.2.0 of the support library to help compute the difference between the old and new list. Details
Don't preload images via glide if your image sizes are different. Try creating your own. Also try looking at.
Create color as the class member
int color = Color.parseColor("#dddddd");
Write View.GONE or View.VISIBLE in Post pojo itself, which will be executed in background thread from Retrofit if IntentService. Try api return boolean in json instead "0" as String.
Move all below to IntentService
//don't display 0 if there are no likes, just show heart icon
if (viewHolder.likesTextView.getText().equals("0"))
viewHolder.likesTextView.setVisibility(View.GONE);
else
viewHolder.likesTextView.setVisibility(View.VISIBLE);
//don't display 0 if there are no footprints
if (viewHolder.footprints.getText().equals("0"))
viewHolder.footprints.setVisibility(View.GONE);
else
viewHolder.footprints.setVisibility(View.VISIBLE);
double[] loc = post.getLocation().get(0);
viewHolder.distance.setText("~" + PostListFragment.distance(loc[0], loc[1], 'M') + " Miles");
All String concatenation also in Post or IntnetService like:
String profilePictureS3Url = "https://s3-us-west-2.amazonaws.com/moleheadphotos/" + post.getUsername() + ".jpg";
Also you can create color filter in advance and one time only. Remove scrollbar from listview as it calculates height to show scroll bar.
Too many things to improve here. Here is some examples.
I see this
if (Hawk.count() == 0)
initHawkWithDataFromServer();
I believe that the method initHawkWithDataFromServer will be called many times during the time the list appears.
This call can be done only once when the activity was created.
Glide.with(mContext).load(videoThumbURL).fitCenter()
But you should refactor your code first, moving the logic to another class. Try to remove some code like this (it should be done by using some layout attribites)
ViewGroup.LayoutParams params = viewHolder.listphoto.getLayoutParams();
Resources r = mContext.getResources();
height = (int) getHeight(height, width);
params.height = height;
params.width = ViewGroup.LayoutParams.MATCH_PARENT;
viewHolder.listphoto.setLayoutParams(params);
this is my first time writing here. I am learning Android development and trying to make a stopwatch (countdown timer) app, which has 12 rounds and pause - example 10 sec between them. I wrote a code which works fine but I want to write it more "readable and nicely" so I would like to ask you for your help. As you can see, I don't want to write every time under public void run() in if-else if statement new roundtime number and setting text because it would take too much space. That is why I wrote it just for example(not 12 times) so you can see what I mean. Do you have any suggestions how to write it better for 12 rounds? Can I use a loop? If u have easier way for countdown round after round I would appreciate it. I am sorry for my english. Any comments are welcome. Enjoy coding.
This is my code:
public class Main3Activity extends AppCompatActivity {
Handler handler;
TextView textView;
ImageView imageView2;
ImageView imageView3;
Boolean checker=false;
long secondtime=1000;
long roundtime1= 10000;
long roundtime2=10000;
long pause= 31000;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main3);
textView=(TextView)findViewById(R.id.textView3);
imageView2=(ImageView)findViewById(R.id.imageView2);
imageView3=(ImageView)findViewById(R.id.imageView3);
imageView2.setOnClickListener(
new ImageView.OnClickListener(){
#Override
public void onClick(View v) {
handler = new Handler();
if (checker == false) {
handler.post(r);
checker = true;
}
imageView3.setOnClickListener(
new ImageView.OnClickListener() {
#Override
public void onClick(View v) {
handler.removeCallbacks(r);
}
}
);
}
final Runnable r= new Runnable() {
#Override
public void run() {
roundtime = roundtime - secondtime;
int seconds = (int) (roundtime / 1000);
int minutes = seconds / 60;
seconds = seconds % 60;
if (roundtime >= 0) {
textView.setText(String.format("%d:%02d", minutes, seconds));
handler.postDelayed(r, 1000);
} else if (pause > 0) {
pause = pause - secondtime;
int secondss = (int) (pause / 1000);
int minutess = secondss / 60;
secondss = secondss % 60;
textView.setText(String.format("%d:%02d", minutess, secondss));
handler.postDelayed(r, 1000);
} else if (roundtime2 > 0) {
roundtime2 = roundtime2 - secondtime;
int secondsss = (int) (roundtime2 / 1000);
int minutesss = secondsss / 60;
secondsss = secondsss % 60;
textView.setText(String.format("%d:%02d", minutesss, secondsss));
handler.postDelayed(r, 1000);
}else{
textView.setText("Over");
}
}
};
}
);
}
}
I'm learning android development, and what I'm trying to do is to have a label that counts down from 40 minutes, and when it reaches 0 it would stop counting and do something else. This is my code:
#Override
protected void onStart() {
super.onStart();
count = 2400;
final Timer t = new Timer();//Create the object
t.scheduleAtFixedRate(new TimerTask() {
#Override
public void run() {
minLeft = (int) Math.floor(count / 60);
secLeft = count - minLeft * 60;
counter = minLeft + ":" + secLeft;
TextView tv = (TextView) findViewById(R.id.timer);
Log.i(MainActivity.TAG,minLeft+", "+secLeft+", "+counter);
tv.setText(counter);
count--;
if (minLeft <= 0 && secLeft <= 0) {
t.cancel();
count = 2400;
onFinish();
}
}
}, 1000, 1000);
}
But, when I go to that activity by clicking a button in the main activity, the label has the text "Timer" (its original text), and after a few seconds the app crashes with CalledFromWrongThreadException, but the line that causes the problem seems to be the one where I set the text of the TextView.
Please help, thanks in advance.
Your scheduled task runs on the background thread.
And you try to set the text to the textview from this background thread.
However in Android all view related operations have to be done on the main thread.
That is way in your scheduled task you have to use something like:
#Override
protected void onStart() {
super.onStart();
count = 2400;
final Timer t = new Timer();//Create the object
t.scheduleAtFixedRate(new TimerTask() {
#Override
public void run() {
minLeft = (int) Math.floor(count / 60);
secLeft = count - minLeft * 60;
counter = minLeft + ":" + secLeft;
// the name of your actual activity
MyActivity.this.runOnUiThread(new Runnable() {
#Override
public void run() {
TextView tv = (TextView) findViewById(R.id.timer);
Log.i(MainActivity.TAG,minLeft+", "+secLeft+", "+counter);
tv.setText(counter);
}
});
count--;
if (minLeft <= 0 && secLeft <= 0) {
t.cancel();
count = 2400;
onFinish();
}
}
}, 1000, 1000);
}
Please also note, that this code can be written more elegantly, without all/so many anonymous classes, but it should do the trick.