Related
To begin with I have tried a lot of ways to make a smooth animation in Android and probably my best option was to use AnimationDrawable. Everything was perfect until I got out of memory exception on older devices. The reason for that obviously is the number of frames, in my case 75. That is how I got to the point of using AsyncTask and Thread.sleep() to animate the frames. To avoid animation lag I used a Stack in which I preload the first 10 frames and then just pop the used one and push a new one until there are no more frames. Everything worked better than I expected, but the only problem is that at the end of the animation the last frame disappears and I am hitting my head whole day to understand why is that happening with no success obviously. Below is the code from the Activity in which I call the animation and the file where the animation code is.
SplashActivity.java
private void startAnimation() {
gifImageView = (LogoAnimImageView) findViewById(R.id.gifImageView);
gifImageView.setSplashActivityContext(this);
gifImageView.setBackgroundResource(R.drawable.logo_frame_0);
gifImageView.setAnimImageViewListener(new LogoAnimImageView.LogoAnimImageViewInterface() {
#Override
public void animationEnd() {
mAnimationFinished = true;
LoadNextActivity();
}
});
gifImageView.startLogoAnimation();
}
LogoAnimImageView.java
public class LogoAnimImageView extends ImageView {
public interface LogoAnimImageViewInterface {
void animationEnd();
}
final Handler mHandler = new Handler();
private Stack<Drawable> mImageStack;
private SplashActivity mSplashActivity;
private LogoAnimImageViewInterface mListener;
private int mFrameIndex;
private int[] mResources = {R.drawable.logo_frame_0,R.drawable.logo_frame_1,R.drawable.logo_frame_2,R.drawable.logo_frame_3,
R.drawable.logo_frame_4,R.drawable.logo_frame_5,R.drawable.logo_frame_6,
R.drawable.logo_frame_7,R.drawable.logo_frame_8,R.drawable.logo_frame_9,R.drawable.logo_frame_10,
R.drawable.logo_frame_11,R.drawable.logo_frame_12,R.drawable.logo_frame_13,R.drawable.logo_frame_14,
R.drawable.logo_frame_15,R.drawable.logo_frame_16,R.drawable.logo_frame_17,R.drawable.logo_frame_18,
R.drawable.logo_frame_19,R.drawable.logo_frame_20,R.drawable.logo_frame_21,R.drawable.logo_frame_22,
R.drawable.logo_frame_23,R.drawable.logo_frame_24,R.drawable.logo_frame_25,R.drawable.logo_frame_26,
R.drawable.logo_frame_27,R.drawable.logo_frame_28,R.drawable.logo_frame_29,R.drawable.logo_frame_30,
R.drawable.logo_frame_31,R.drawable.logo_frame_32,R.drawable.logo_frame_33,R.drawable.logo_frame_34,
R.drawable.logo_frame_35,R.drawable.logo_frame_36,R.drawable.logo_frame_37,R.drawable.logo_frame_38,
R.drawable.logo_frame_39,R.drawable.logo_frame_40,R.drawable.logo_frame_41,R.drawable.logo_frame_42,
R.drawable.logo_frame_43,R.drawable.logo_frame_44,R.drawable.logo_frame_45,R.drawable.logo_frame_46,
R.drawable.logo_frame_47,R.drawable.logo_frame_48,R.drawable.logo_frame_49,R.drawable.logo_frame_50,
R.drawable.logo_frame_51,R.drawable.logo_frame_52,R.drawable.logo_frame_53,R.drawable.logo_frame_54,
R.drawable.logo_frame_55,R.drawable.logo_frame_56,R.drawable.logo_frame_57,R.drawable.logo_frame_58,
R.drawable.logo_frame_59,R.drawable.logo_frame_60,R.drawable.logo_frame_61,R.drawable.logo_frame_62,
R.drawable.logo_frame_63,R.drawable.logo_frame_64,R.drawable.logo_frame_65,R.drawable.logo_frame_66,
R.drawable.logo_frame_67,R.drawable.logo_frame_68,R.drawable.logo_frame_69,R.drawable.logo_frame_70,
R.drawable.logo_frame_71,R.drawable.logo_frame_72,R.drawable.logo_frame_73,R.drawable.logo_frame_74
};
public LogoAnimImageView(Context context) {
super(context);
}
public LogoAnimImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public LogoAnimImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void startLogoAnimation() {
mFrameIndex = 10;
mImageStack = new Stack<Drawable>();
for (int i=1;i<=mFrameIndex;i++) {
Drawable drawable = getDrawable(mResources[i]);
mImageStack.push(drawable);
}
mFrameIndex++;
mSplashActivity.runOnUiThread(new Runnable() {
#Override
public void run() {
new LogoAnimOperation().execute((Object)null);
}
});
}
public void setSplashActivityContext(SplashActivity splashActivity) {
this.mSplashActivity = splashActivity;
}
public void setAnimImageViewListener(LogoAnimImageViewInterface listener) {
this.mListener = listener;
}
private Drawable getDrawable(int id) {
Drawable drawable;
if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP){
drawable = mSplashActivity.getDrawable(id);
} else {
drawable = mSplashActivity.getResources().getDrawable(id);
}
return drawable;
}
private class LogoAnimOperation extends AsyncTask<Object,Void,String> {
#Override
protected String doInBackground(Object... params) {
int number=1;
while (mImageStack.size() > 1) {
try {
Thread.sleep(40);
} catch (InterruptedException e) {
e.printStackTrace();
}
final Drawable drawable = mImageStack.pop();
mSplashActivity.runOnUiThread(new Runnable() {
#Override
public void run() {
if(android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
LogoAnimImageView.this.setBackground(drawable);
}
else {
LogoAnimImageView.this.setBackgroundDrawable(drawable);
}
if (mFrameIndex < mResources.length) {
Drawable newDrawable = getDrawable(mResources[mFrameIndex]);
mImageStack.push(newDrawable);
mFrameIndex++;
}
}
});
}
return "";
}
#Override
protected void onPostExecute(String s) {
mSplashActivity.runOnUiThread(new Runnable() {
#Override
public void run() {
Drawable drawable = getDrawable(R.drawable.logo_frame_74);
if(android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
LogoAnimImageView.this.setBackground(drawable);
}
else {
LogoAnimImageView.this.setBackgroundDrawable(drawable);
}
}
});
mListener.animationEnd();
super.onPostExecute(s);
}
}
}
...but the only problem is that at the end of the animation the last
frame disappears and I am hitting my head whole day to understand why
is that happening with no success obviously.
The problem may lie in your AsyncTask's onPostExecute(String):
#Override
protected void onPostExecute(String s) {
mSplashActivity.runOnUiThread(new Runnable() {
#Override
public void run() {
Drawable drawable = getDrawable(R.drawable.logo_frame_74);
if(android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
LogoAnimImageView.this.setBackground(drawable);
} else {
LogoAnimImageView.this.setBackgroundDrawable(drawable);
}
}
});
mListener.animationEnd();
super.onPostExecute(s);
}
onPostExecute(String) will always be called on the UI thread. So, mSplashActivity.runOnUiThread(....) is redundant.
By using runOnUiThread(Runnable), you are posting to the UI thread's event queue. So, the runnable is executed when its turn comes up. However, the code after the mSplashActivity.runOnUiThread(....) call may get executed before the runnable. So, mListener.animationEnd() may be getting called before your LogoAnimImageView has a chance to display R.drawable.logo_frame_74.
But, this should not happen in your case. If runOnUiThread(Runnable) is called from the UI thread (which, it is), the Runnable is not posted to the event queue, and executed immediately instead.
I suspect that the real issue here is that there isn't any delay between the last frame of your animation (R.drawable.logo_frame_74), and launch of next activity. Perhaps you could comment out the call to mListener.animationEnd(), to check whether the animation ends at the last or second-last frame.
Although this is an interesting approach, and one I haven't seen before, I have to say that you are meddling with more threads than you need to. If you're trying to load Drawables as and when they are needed, there is a simpler way:
public class LogoAnimImageView extends ImageView {
....
....
// flag to indicate whether `mNextFrameDrawable` should continue loading the next frame
private boolean mStopAnimating;
// loads the next frame, and calls back to activity when done
private Runnable mNextFrameRunnable = new Runnable() {
#Override
public void run() {
if (!mStopAnimating) {
if (isFinishedAnimating() && mListener != null) {
mListener.animationEnd();
} else { // Load next frame
setViewBg(getNextFrameDrawable());
// Will load the next frame in 40 ms
postDelayed(this, 40L);
}
}
}
};
// This method can be set `public static` and placed in a separate `Utils` class
private void setViewBg(Drawable d) {
if(android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
setBackground(drawable);
} else {
setBackgroundDrawable(drawable);
}
}
private Boolean isFinishedAnimating() {
return mFrameIndex >= mResources.length;
}
// returns the next frame's drawable and increments the `mFrameIndex` pointer
private Drawable getNextFrameDrawable() {
return getDrawable(mResources[mFrameIndex++]);
}
// start animating
public void startLogoAnimation() {
mFrameIndex = 0;
mStopAnimating = false;
post(mNextFrameRunnable);
}
// stop animating
public void stopLogoAnimation() {
mStopAnimating = true;
removeCallbacks(mNextFrameRunnable);
}
....
....
}
AsyncTask is neither needed, nor designed to handle such scenarios.
I am trying to update my listview to make an "endless scroll". What happens is that the first 40 results load fine, when i get to the bottom of the scroll, next 40 results replace the first 40...
What I want is for second set of 40 results to add to the first 40 so I have an endless list and ability to scroll back to the beginning of the list.
I am posting my code below. Thank you!
public class SearchResults extends Activity implements BannerAdListener, OnScrollListener{
private LinearLayout bottomNav;
private ListView ringtoneList;
private int start = 0, num = 40, curPage = 1;
private Handler handler = new Handler();
private ProgressDialog progressDialog = null;
private ArrayList<Ringtone> ringtones;
private MoPubView moPubView;
private String searchString;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle extras = getIntent().getExtras();
if (extras == null) {
// no search string defined
finish();
} else {
searchString = extras.getString("search_string");
}
setContentView(R.layout.search_results);
ringtoneList = (ListView)findViewById(R.id.ringtone_list);
ringtoneList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> av, View view, int position, long id) {
Intent i = new Intent(SearchResults.this, RingtoneView.class);
i.putExtra("ringtone", ringtones.get(position));
startActivity(i);
}
});
performSearch();
moPubView = (MoPubView) findViewById(R.id.adview);
moPubView.setAdUnitId(Utils.MoPubBannerId);
moPubView.loadAd();
moPubView.setBannerAdListener(this);
ringtoneList.setOnScrollListener(this);
}
public void onScroll(AbsListView view, int firstVisible, final int visibleCount, int totalCount) {
Log.i("List", "firstVisible="+firstVisible+" visibleCount="+visibleCount+" totalCount="+totalCount);
boolean loadMore = firstVisible + visibleCount >= totalCount;
if(loadMore) {
Log.i("List", "Loading More Results");
curPage++;
start = num * (curPage-1);
new Thread() {
public void run() {
ringtones = Utils.search(start, num, searchString);
if (ringtones != null && ringtones.size() > 0) {
handler.post(new Runnable() {
public void run() {
ringtoneList.setAdapter(new RingtoneRowAdapter(SearchResults.this, ringtones));
}
});
} else {
handler.post(new Runnable() {
public void run() {
new AlertDialog.Builder(SearchResults.this)
.setTitle(getResources().getString(R.string.context_info)).setMessage(getResources().getString(R.string.context_noresult))
.setPositiveButton(getResources().getString(R.string.context_ok), null).show();
}
});
}
}
}
.start();
}
}
public void onScrollStateChanged(AbsListView v, int s) { }
private void performSearch() {
progressDialog = ProgressDialog.show(SearchResults.this, getResources().getString(R.string.loading_message), getResources().getString(R.string.loading_search), true);
new Thread() {
public void run() {
ringtones = Utils.search(start, num, searchString);
if (ringtones != null && ringtones.size() > 0) {
updateList();
} else {
handler.post(new Runnable() {
public void run() {
new AlertDialog.Builder(SearchResults.this)
.setTitle(getResources().getString(R.string.context_info)).setMessage(getResources().getString(R.string.context_noresult))
.setPositiveButton(getResources().getString(R.string.context_ok), null).show();
ringtoneList.setVisibility(View.INVISIBLE);
bottomNav.setVisibility(View.INVISIBLE);
}
});
}
progressDialog.dismiss();
}
}.start();
}
private void updateList() {
handler.post(new Runnable() {
public void run() {
//Log.d("search", "ringtones.size() " + ringtones.size());
ringtoneList.setVisibility(View.VISIBLE);
ringtoneList.setAdapter(new RingtoneRowAdapter(SearchResults.this, ringtones));
}
});
}
}
Please help! Thank you!
While I can't be 100% sure, I think your problem has to do with the fact that you're setting a new adapter that only has the section of ringtones that in loads. It probably has to do with this snippet:
ringtones = Utils.search(start, num, searchString);
if (ringtones != null && ringtones.size() > 0) {
handler.post(new Runnable() {
public void run() {
ringtoneList.setAdapter(new RingtoneRowAdapter(SearchResults.this, ringtones));
}
});
}
Instead of putting an entirely new array of ringtones, you should add on to the one you already have. Your ringtones variable is already an instance variable, so I'm sure if you changed this line:
ringtones = Utils.search(start, num, searchString);
to the following:
ringtones.addAll(Utils.search(start, num, searchString));
It might fix your problem.
Your code is a little bit messy, but your updateList method is creating a NEW RingtoneRowAdapter. You should ADD items to the list and call
mRingtoneRowAdapter.notifyDatasetChanged();
That will tell the adapter to get new views (if needed) along with a lot of internal stuff happening at Adapter's level. So it's kinda:
private void updateList() {
handler.post(new Runnable() {
public void run() {
if ( mAdapter == null ) {
mAdapter = new RingtoneRowAdapter(SearchResults.this, ringtones);
ringtoneList.setAdapter(mAdapter);
} else {
mAdapter.notifyDataSetChanged();
}
ringtoneList.setVisibility(View.VISIBLE);
}
});
}
And of course, as already suggested, don't init your ringtones array all the time. Just add data to it.
You don't need (most of the times) to hide the list, you can add in your layout, any View (including a full Layout!) with an id of:
android:id="#android:id/empty"
And if your ListView has an id of android:id="#id/android:list", and you're in a ListFragment or ListActivity, then when the list is empty, the "empty" layout will be shown (which can be a dummy view if you don't want to see it).
Just suggestions :)
UPDATE: For your null, I see that the logic is a little bit weird and since we don't know the requirements for your app (i.e. what to do if there are no results, what to do if the results are invalid, etc.) I'll assume you just want to make sure it works and deal with that later.
So with that in mind, I see two problems.
I assume your Utils.search method can return null, because you're checking for it. To me that feels strange, I'd rather return an empty array indicating that the search produced no results; that little remark aside, you are not checking (or we don't know because we haven't seen the source for your search method), if the searchString is null or not.
You haven't provided a stack trace, so we can't tell where the null is happening (either inside search or ?)
A quick solution would be to check for null before searching… I would personally do this INSIDE the search function.
Something like:
public ArrayList<Ringtone> search(final int start, final int num, final String searchString) {
if ( searchString == null ) {
//DECIDE WHAT YOU WANT TO DO, EITHER:
return null;
// OR YOU CAN RETURN AN EMPTY ARRAY
return new ArrayList<Ringtone>();
}
// You should check for these (change according to your rules)
if (start < 0 ) {
start = 0; // protect yourself from bad data.
}
if ( num < 0 ) {
num = 0;
}
/// THE REST OF YOUR search FUNCTION
return <your array>
}
Now another thing is that you may want the search to return incremental results (so you can ADD them to your array (instead of returning a new array every time). For that, as already suggested, use the addAll trick, but then, DON'T return null, return an new empty array, so there's no harm done if there's nothing else to add.
I am facing multitouch issue. The problem is I can simultaneously touch two buttons on my screen. I know this question is asked several times in this forum and the only solution is to declare android:splitMotionEvents="false" in your parent layout. But after declaring this the issue remains. Is it the problem with the hardware or is it with the code ? Any pointer here is appreciated.
This issue appears beacuse since android 4.0 each onClick performed in a new thread.
How i solved it:
//1. create your own click listener
public abstract class AbstractCarOnClickListener {
protected static volatile boolean processing = false;
protected void executeBlock() {
ActivityUtil.scheduleOnMainThread(new Runnable() {
public void run() {
processing=false;
}
}, 400);
}
}
//2. create subclass of your listener
public abstract class AppButtonsOnClickListener extends AbstractCarOnClickListener implements View.OnClickListener {
public void onClick(View v) {
if(processing) return;
try{
processing=true;
onCarButtonClick(v);
} finally {
executeBlock();
}
}
public abstract void onCarButtonClick(View v);
}
//3. set listener to your view
public void onClick(View v) {
clickListener.onClick(v);
}
public OnClickListener clickListener = new AppButtonsOnClickListener(){
public void onCarButtonClick(View v) {
hintContainer.setVisibility(View.GONE);
if (v == cancelButton) {
listener.onCancelButtonClicked();
}
}
}
This is what worked for me. In addition to setting the android:splitMotionEvents="false" on every ViewGroup that contains the buttons I put this in MyAdapter.getView()...
view.setOnTouchListener(new OnTouchListener() {
#Override
public boolean onTouch(View cell, MotionEvent event) {
// Process touches only if: 1) We havent touched a cell, or 2) This event is on the touched cell
if (mTouchedCell == null || cell.equals(mTouchedCell)) {
int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) {
cell.startAnimation(mShrink);
mTouchedCell = cell;
} else if (action == MotionEvent.ACTION_CANCEL) {
if (cell.equals(mTouchedCell)) {
cell.startAnimation(mGrow);
}
mTouchedCell = null;
return true;
} else if (action == MotionEvent.ACTION_UP) {
cell.startAnimation(mFadeOut);
mTouchedCell = null;
}
return false;
}
return true;
}
});
...and of course this in the adapter...
private static View mTouchedCell = null;
on a child layout (View) is there a callback for when the view is removed from it's parent? I need to recycle some images when the view is done. I've been looking around on the web for what to do, but haven't found anything helpful yet.
I've been looking for something like this too. The best I can find is View.OnAttachStateChangeListener. I doubt it's ideal, as it's the callback for when the View is added & removed from the Window - not the parent, but it's sufficient for my needs.
Instead of registering a new listener, you can override onDetachedFromWindow in your custom View code.
I fall in that trap what marmor said:)
#Override
protected void onDetachedFromWindow() { I want to do something here, sometimes called sometimes not!!}
protected void onAttachedToWindow() {It is working fine, always}
This code is in a CustomView.
The calling code is:
contentHolder.removeAllViews();
// ... init my CustomView ...
contentHolder.addView(myCustomView);
contentHolder.requestLayout();// useless, not need
contentHolder.invalidate();// useless, not need
To understand why is not working you have to go inside Android API:
public void removeAllViews() {
removeAllViewsInLayout();
requestLayout();
invalidate(true);
}
public void removeAllViewsInLayout() {
final int count = mChildrenCount;
if (count <= 0) {
return;
}
final View[] children = mChildren;
mChildrenCount = 0;
final View focused = mFocused;
final boolean detach = mAttachInfo != null;
boolean clearChildFocus = false;
needGlobalAttributesUpdate(false);
for (int i = count - 1; i >= 0; i--) {
final View view = children[i];
if (mTransition != null) {
mTransition.removeChild(this, view);
}
if (view == focused) {
view.unFocus(null);
clearChildFocus = true;
}
view.clearAccessibilityFocus();
cancelTouchTarget(view);
cancelHoverTarget(view);
if (view.getAnimation() != null ||
(mTransitioningViews != null && mTransitioningViews.contains(view))) {
addDisappearingView(view);
} else if (detach) {
view.dispatchDetachedFromWindow();
}
if (view.hasTransientState()) {
childHasTransientStateChanged(view, false);
}
dispatchViewRemoved(view);
view.mParent = null;
children[i] = null;
}
if (clearChildFocus) {
clearChildFocus(focused);
if (!rootViewRequestFocus()) {
notifyGlobalFocusCleared(focused);
}
}
}
The key is here:
if (view.getAnimation() != null ||
(mTransitioningViews != null && mTransitioningViews.contains(view))) {
So, if you have animation ( and in 1 case I have and in 9 cases not) it will not called the onDetachedFromWindow() and it will mess the whole UI :)
public void endViewTransition(View view) {
if (mTransitioningViews != null) {
mTransitioningViews.remove(view);
final ArrayList<View> disappearingChildren = mDisappearingChildren;
if (disappearingChildren != null && disappearingChildren.contains(view)) {
disappearingChildren.remove(view);
if (mVisibilityChangingChildren != null &&
mVisibilityChangingChildren.contains(view)) {
mVisibilityChangingChildren.remove(view);
} else {
if (view.mAttachInfo != null) {
view.dispatchDetachedFromWindow();
}
if (view.mParent != null) {
view.mParent = null;
}
}
invalidate();
}
}
}
Again in some cases will be called even with animation.
addDisappearingView(view);
The accepted answer suggest something like this:
addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
#Override
public void onViewAttachedToWindow(View v) {
}
#Override
public void onViewDetachedFromWindow(View v) {
System.out.println("MyCustomView.onViewDetachedFromWindow");
}
});
Sadly on animation will not print the desired text.
Some important code from android.view.ViewGroup API:
void dispatchViewRemoved(View child) {
onViewRemoved(child);
if (mOnHierarchyChangeListener != null) {
mOnHierarchyChangeListener.onChildViewRemoved(this, child);
}
}
public void onViewRemoved(View child) {
}
So, you can override your RelativeLayout for this method.
My animation is an infinite animation, and it will not be called very soon any of the methods!
If you have an infinite animation the correct way is to write this code, when you call remove all views:
if(contentHolder.getChildCount() > 0 ){
View child0 = contentHolder.getChildAt(0);
Animation animation = child0.getAnimation();
if(animation != null) {
animation.cancel();
child0.clearAnimation();
}
}
contentHolder.removeAllViews();
Now it will be called the protected void onDetachedFromWindow()!
The Android KTX (Core KTX) library gives you a nice solution for this.
You'll need this dependency: androidx.core:core-ktx:1.3.0
You can then call a function "doOnDetach" to signal you want to run some code (once) when the view is removed from the window:
fun myInitCode() {
...
myView.doOnDetach(this::doOnMyViewDetachFromWindow)
...
}
fun doOnMyViewDetachFromWindow(view: View) {
... put your image cleanup code here ...
}
You can pass a lambda to "doOnDetach" but a method reference as shown above may be cleaner, depending on how much work you have to do.
The description of doOnDetach is as follows:
androidx.core.view ViewKt.class public inline fun View.doOnDetach(
crossinline action: (View) → Unit ): Unit
Performs the given action when this view is detached from a window. If
the view is not attached to a window the action will be performed
immediately, otherwise the action will be performed after the view is
detached from its current window. The action will only be invoked
once, and any listeners will then be removed.
I'm new to Android so sorry if the question is easy to answer. I have two buttons, a decrease and an increase button, and in the middle of them a TextView which displays a value.
When I hit the decrease button, the value in the TextView decreases and increases when I hit the increase button, no problem with that, I got that working but the problem is the value only increases/decreases by 1 on a single click. What I'm trying to achieve is that as I continuously press the button (the increase button for example), the value is also continuously increasing and only stops when I release the increase button.
Is that possible? If so, can you show some sample code or references on how to implement that? Thanks!
Here is my main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center" >
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="44dp"
android:gravity="center_horizontal" >
<Button android:id="#+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:text="<" />
<TextView android:id="#+id/textView1"
android:layout_width="50dp"
android:layout_height="fill_parent"
android:layout_alignBottom="#+id/button1"
android:layout_toRightOf="#+id/button1"
android:gravity="center"
android:text="45" />
<Button android:id="#+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_toRightOf="#+id/textView1"
android:text=">" />
</RelativeLayout>
</RelativeLayout>
and here is my Main.java
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
public class Main extends Activity {
private Button _decrease;
private Button _increase;
private TextView _value;
private static int _counter = 45;
private String _stringVal;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
_decrease = (Button) findViewById(R.id.button1);
_increase = (Button) findViewById(R.id.button2);
_value = (TextView) findViewById(R.id.textView1);
_decrease.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
Log.d("src", "Decreasing value...");
_counter--;
_stringVal = Integer.toString(_counter);
_value.setText(_stringVal);
}
});
_increase.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
Log.d("src", "Increasing value...");
_counter++;
_stringVal = Integer.toString(_counter);
_value.setText(_stringVal);
}
});
}
}
For that to work, you need a thread that will update the integer value when you long press on a button.
Create a handler in your activity:
private Handler repeatUpdateHandler = new Handler();
And 2 vars which will state: is it increment or decrement? Only one set at a time.
private boolean mAutoIncrement = false;
private boolean mAutoDecrement = false;
And the present number value
public int mValue;
And a class that will run in another thread:
class RptUpdater implements Runnable {
public void run() {
if( mAutoIncrement ){
increment();
repeatUpdateHandler.postDelayed( new RptUpdater(), REP_DELAY );
} else if( mAutoDecrement ){
decrement();
repeatUpdateHandler.postDelayed( new RptUpdater(), REP_DELAY );
}
}
}
Add a long press listener to your button:
mBTIncrement.setOnLongClickListener(
new View.OnLongClickListener(){
public boolean onLongClick(View arg0) {
mAutoIncrement = true;
repeatUpdateHandler.post( new RptUpdater() );
return false;
}
}
);
mBTIncrement.setOnTouchListener( new View.OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
if( (event.getAction()==MotionEvent.ACTION_UP || event.getAction()==MotionEvent.ACTION_CANCEL)
&& mAutoIncrement ){
mAutoIncrement = false;
}
return false;
}
});
In the above case the button is the increment one. Create another button which will set mAutoDecrement to true.
And decrement() will be a function, which will set your instance int variable like this:
public void decrement(){
mValue--;
_value.setText( ""+mValue );
}
You figure the increment out. Oh and REP_DELAY is a static int variable set to 50.
I see this is an excerpt from Jeffrey Cole's open source NumberPicker available at http://www.technologichron.net/ Proper author's attribution must be added.
While the accepted answer is totally correct, it can be simplified a bit.
Basically, we can optimize two things:
We don't need the OnTouchListener.
We can instantiate the runnable object just once instead of creating multiple objects.
So this is my version:
// global variables
Handler handler = new Handler();
Runnable runnable;
increaseView.setOnLongClickListener(new View.OnLongClickListener() {
#Override
public boolean onLongClick(View v) {
runnable = new Runnable() {
#Override
public void run() {
if (!increaseView.isPressed()) return;
increaseValue();
handler.postDelayed(runnable, DELAY);
}
};
handler.postDelayed(runnable, DELAY);
return true;
}
});
Here the runnable object is reused. And when the view is not pressed anymore, it will stop calling itself.
The decrease view or button can be defined in a similar way.
I am late to answer this question but it may help any one who's looking for a better answer.
I created a CounterHandler class and its insanely easy to use to achieve the above mentioned continuous counter functionality.
You can find the class in following gist with a "how to use" example.
https://gist.github.com/nomanr/d142f4ccaf55ceba22e7f7122b55b9b6
Sample code
new CounterHandler.Builder()
.incrementalView(buttonPlus)
.decrementalView(buttonMinus)
.minRange(-50) // cant go any less than -50
.maxRange(50) // cant go any further than 50
.isCycle(true) // 49,50,-50,-49 and so on
.counterDelay(200) // speed of counter
.counterStep(2) // steps e.g. 0,2,4,6...
.listener(this) // to listen counter results and show them in app
.build();
Thats all. :)
My way to increment value on long click is to use Timer used to check periodically if button is still pressed and than increase value, otherwise cancel timer. To update UI use Handler.
vh.bttAdd.setOnLongClickListener(new View.OnLongClickListener() {
#Override
public boolean onLongClick(View v) {
final Timer timer = new Timer();
timer.schedule(new TimerTask() {
#Override
public void run() {
if(vh.bttAdd.isPressed()) {
final int track = ((ChannelAudioTrack) channels.get(vh.getAdapterPosition())).goToNextTrack();
updateUI(vh,track);
}
else
timer.cancel();
}
},100,200);
return true;
}
});
Handler:
private void updateUI(final TrackViewHolder vh, final int track)
{
new Handler(Looper.getMainLooper()).post(new Runnable() {
#Override
public void run() {
vh.tvTrackNumber.setText(Integer.toString(track));
}
}) ;
}
Just wanna share my own solution that worked out for me really well.
First, create a handler in your activity
private Handler mHandler = new Handler();
Then, create the runnables that will increment/decrement and display your number. In here, we will check if your button is still in its pressed state and increment then re-run the runnable if it is.
private Runnable incrementRunnable = new Runnable() {
#Override
public void run() {
mHandler.removeCallbacks(incrementRunnable); // remove our old runnable, though I'm not really sure if this is necessary
if(IncrementButton.isPressed()) { // check if the button is still in its pressed state
// increment the counter
// display the updated value here, if necessary
mHandler.postDelayed(incrementRunnable, 100); // call for a delayed re-check of the button's state through our handler. The delay of 100ms can be changed as needed.
}
}
}
Finally, use it in our button's onLongClickListener
IncrementButton.setOnLongClickListener(new View.OnLongClickListener() {
#Override
public boolean onLongClick(View view) {
mHandler.postDelayed(incrementRunnable, 0); // initial call for our handler.
return true;
}
});
That's it!
Another way of doing it is declaring both the handler and runnable inside the OnLongClickListener itself although I myself am not sure if this is a good practice.
IncrementButton.setOnLongClickListener(new View.OnLongClickListener() {
private Handler mHandler = Handler();
private Runnable incrementRunnable = new Runnable() {
#Override
public void run() {
mHandler.removeCallbacks(incrementRunnable);
if(IncrementButton.isPressed()) {
// increment the counter
// display the updated value here, if necessary
mHandler.postDelayed(incrementRunnable, 100);
}
}
};
#Override
public boolean onLongClick(View view) {
mHandler.postDelayed(incrementRunnable, 0);
return true;
}
});
When doing this continuous increment, I would suggest to increase the increment value after a certain time/number of increments.
E.g. If the number_of_increment made is less than 10, we increment by 1. Otherwise, we increment by 3.
It seems there is no perfect solution to this question and there will always be some complexity involved.
This is my attempt, which incorporates Wiktor's answer, but gives a whole MainActivity that you can cut/paste.
In my example, the complex part is the onLongClickListener, and how deep it goes and how many levels of anonymous classes there are.
However, on the other hand, the simplicity is that everything is included in one relatively short class (MainActivity), and there is only one major block of code -- the onLongClickListener -- which is defined only once and it's very clear where the "action" code is:
package com.example.boober.aalongclickoptimizationunit;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import java.util.Timer;
import java.util.TimerTask;
public class MainActivity extends AppCompatActivity {
TextView valueDisplay;
Button minusButton;
Button plusButton;
Button[] arrayOfControlButtons;
Integer currentDisplayValue = 500;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
valueDisplay = findViewById(R.id.value);
minusButton = findViewById(R.id.minusButton);
plusButton = findViewById(R.id.plusButton);
arrayOfControlButtons = new Button[]{plusButton, minusButton}; // this could be a large set of buttons
updateDisplay(); // initial setting of display
for (Button b : arrayOfControlButtons) {
b.setOnLongClickListener(new View.OnLongClickListener() {
#Override
public boolean onLongClick(final View v) {
final Timer timer = new Timer();
timer.schedule(new TimerTask() {
#Override
public void run() {
if (v.isPressed()) { // important: checking if button still pressed
runOnUiThread(new Runnable() {
#Override
public void run() {
// --------------------------------------------------
// this is code that runs each time the
// long-click timer "goes off."
switch (v.getId()) {
// which button was pressed?
case R.id.plusButton: {
currentDisplayValue = currentDisplayValue + 10;
break;
}
case R.id.minusButton: {
currentDisplayValue = currentDisplayValue - 10;
break;
}
}
updateDisplay();
// --------------------------------------------------
}
});
} else
timer.cancel();
}
}, 100, 200);
// if set to false, then long clicks will propagate into single-clicks
// also, and we don't want that.
return true;
}
});
}
}
// ON-CLICKS (referred to from XML)
public void minusButtonPressed(View ignored) {
currentDisplayValue--;
updateDisplay();
}
public void plusButtonPressed(View ignored) {
currentDisplayValue++;
updateDisplay();
}
// INTERNAL
private void updateDisplay() {
valueDisplay.setText(currentDisplayValue.toString());
}
}
for kotlin user
myButton.setOnLongClickListener {
val handler = Handler(Looper.myLooper()!!)
val runnable : Runnable = object : Runnable {
val number = 0
override fun run() {
handler.removeCallbacks(this)
if (myButton.isPressed) {
val newNumber= number + 1
textView.text = "$newNumber Items"
handler.postDelayed(this, 100)
}
}
}
handler.postDelayed(runnable,0)
true
}
Version for Kotlin and Coroutines users (replace GlobalScope with any other scope if you'd like):
var job: Job? = null
viewClickable.setOnClickListener {
// single click
}
viewClickable.setOnLongClickListener {
if (job == null || !job!!.isActive) {
job = GlobalScope.launch(Dispatchers.Main.immediate) {
while (it.isPressed) {
// long press
delay(100)
}
}
}
true
}
import java.awt.;
import java.awt.event.;
public class LabelNumber extends Frame implements ActionListener
{
Button badd,bsub;
TextField t1;
void display()
{
setTitle("Label number change");
setSize(400,500);
setLayout(new FlowLayout());
setVisible(true);
badd=new Button("+");
t1=new TextField("0",6);
bsub= new Button("-");
add(bsub);add(t1);add(badd);
badd.addActionListener(this);
bsub.addActionListener(this);
addWindowListener(new WindowAdapter()
{
public void windowClosing(WindowEvent e)
{
System.exit(0);
}
}
);
}
public void actionPerformed(ActionEvent e)
{
int count=0,add=0,sub,n1,n2;
count=Integer.parseInt(t1.getText());
if(e.getSource()==badd)
{
if(count<10)
{
count=count+1;
t1.setText(""+count);
}
else
{
t1.setText("limit:10");
}
}
if(e.getSource()==bsub)
{
if(count<10)
{
count=count-1;
t1.setText(""+count);
}
else
{
t1.setText("limit:10");
}
}
}
public static void main(String args[])
{
LabelNumber obj =new LabelNumber();
obj.display();
}
}
initlize and call method
int speed = 0;
button = findViewById(R.id.button);
button.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
v.setPressed(true);
increment();
} else if (event.getAction() == MotionEvent.ACTION_UP) {
v.setPressed(false);
speed = 0;
}
return true;
});
This is increase method
public void increment() {
new Handler().postDelayed(() -> {
Toast.makeText(FrequencyActivity.this, String.valueOf(speed), Toast.LENGTH_SHORT).show();
if (button.isPressed()) {
speed += 1;
increment();
}
}, 200); //200 ms for fast incrementing
}
To break this task into its basic requirements, this is what we need:
Function to be executed
Condition for re-execution (as function that can be called each time to check condition)
Delay before re-execution
Here is a function that can be copied into Utils class, and be called for anything that matches these requirements:
/**
* Execute given function, and if condition is met, re-execute recursively after delay
* #param function: function to be executed
* #param conditionToRepeat: condition to re-execute function
* #param delayMillis: delay after which function should be re-executed (if condition was met)
*/
fun executeRecursively(function: () -> Unit, conditionToRepeat: () -> Boolean, delayMillis: Long) {
function()
if (conditionToRepeat())
Handler(Looper.getMainLooper()).postDelayed(
{ executeRecursively(function, conditionToRepeat, delayMillis) },
delayMillis)
}
Example of usage for requested use-case:
binding.button.setOnLongClickListener {
executeRecursively( { increase() }, // function
{ binding.button.isPressed }, // condition to re-execute
DELAY // delay in millis before re-execution
)
true
}
This is all what you need. But many cases, you may want to decrease delay when long clicking, so the number will be increased/decreased faster. Here is an extended function for that case:
/**
* Execute given function, and if condition is met, re-execute recursively after delay
* #param function: function to be executed
* #param conditionToRepeat: condition to re-execute function
* #param delayMillis: delay after which function should be re-executed (if condition was met)
* #param minDelayMillis: minimal delay in milliseconds
* #param decreasingDelayMillis: amount to decrease delay for next re-execution (if minimal delay has not been reached)
*/
fun executeRecursivelyWithDecreasingDelay(function: () -> Unit, conditionToRepeat: () -> Boolean, delayMillis: Long, minDelayMillis: Long, decreasingDelayMillis: Long) {
function()
if (conditionToRepeat()) {
val delay = if (delayMillis <= minDelayMillis) minDelayMillis else delayMillis - decreasingDelayMillis
Handler(Looper.getMainLooper()).postDelayed(
{ executeRecursivelyWithDecreasingDelay(function, conditionToRepeat, delay, minDelayMillis, decreasingDelayMillis) },
delayMillis
)
}
}
In this function, just add minimal delay (for example 2 millis), and the rate in which delay should be decreased each time (for example 2 millis). decreasingDelayMillis is like delay negative velocity.
Best and easy solution i created ,check it out it works for me
public void increment() {
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
check_amount = check_amount + 100;// increment by 100
tv_balance.setText("" + check_amount); // show continues incrementing value
if (check_amount != 5000) {
increment();
}
}
}, 1); //1 ms for fast incrementing
}