I'm fairly new to Java, and really new to Android development.
I am trying to make a Morse Code app, which will flash the user's entered message in Morse Code on the device's screen.
My problem is that I cannot find a way to pause the code to put the timings in.
I know that putting a wait() or sleep() straight in is out of the question. After some searching around the web, I found some code and implemented it into mine - here is m current code - this is just a countdown to test the waiting:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_flash);
// ALL THIS STUFF IS AUTO-GENERATED
final View controlsView = findViewById(R.id.fullscreen_content_controls);
final View contentView = findViewById(R.id.fullscreen_content);
// Set up an instance of SystemUiHider to control the system UI for
// this activity.
mSystemUiHider = SystemUiHider.getInstance(this, contentView, HIDER_FLAGS);
mSystemUiHider.setup();
mSystemUiHider
.setOnVisibilityChangeListener(new SystemUiHider.OnVisibilityChangeListener() {
// Cached values.
int mControlsHeight;
int mShortAnimTime;
#Override
#TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2)
public void onVisibilityChange(boolean visible) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) {
// If the ViewPropertyAnimator API is available
// (Honeycomb MR2 and later), use it to animate the
// in-layout UI controls at the bottom of the
// screen.
if (mControlsHeight == 0) {
mControlsHeight = controlsView.getHeight();
}
if (mShortAnimTime == 0) {
mShortAnimTime = getResources().getInteger(
android.R.integer.config_shortAnimTime);
}
controlsView.animate()
.translationY(visible ? 0 : mControlsHeight)
.setDuration(mShortAnimTime);
} else {
// If the ViewPropertyAnimator APIs aren't
// available, simply show or hide the in-layout UI
// controls.
controlsView.setVisibility(visible ? View.VISIBLE : View.GONE);
}
if (visible && AUTO_HIDE) {
// Schedule a hide().
delayedHide(AUTO_HIDE_DELAY_MILLIS);
}
}
});
// Set up the user interaction to manually show or hide the system UI.
contentView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
if (TOGGLE_ON_CLICK) {
mSystemUiHider.toggle();
} else {
mSystemUiHider.show();
}
}
});
// Upon interacting with UI controls, delay any scheduled hide()
// operations to prevent the jarring behavior of controls going away
// while interacting with the UI.
// EVERYTHING UP TO HERE IS AUTO-GENERATED
final TextView currentLabel = (TextView) findViewById(R.id.labelCurrent);
final TextView totalLabel = (TextView) findViewById(R.id.labelTotal);
new Thread () {
public void run() {
currentLabel.setText("3");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
currentLabel.setText("2");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
currentLabel.setText("1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
startActivity(new Intent(getApplicationContext(), MainActivity.class));
}
}.start();
}
When testing this code, I found that in the emulated AVD, it just goes straight back to the MainActivity, and when I generated an APK and tested it on my device, the app crashed.
Any help is appreciated.
EDIT 1- Here's the logcat:
08-09 15:42:29.054 2012-2012/com.example.nat.morseflasher D/gralloc_goldfish﹕ Emulator without GPU emulation detected.
08-09 15:42:38.564 2012-2012/com.example.nat.morseflasher D/dalvikvm﹕ GC_FOR_ALLOC freed 143K, 8% free 3195K/3444K, paused 142ms, total 183ms
08-09 15:42:55.654 2012-2012/com.example.nat.morseflasher I/Choreographer﹕ Skipped 31 frames! The application may be doing too much work on its main thread.
08-09 15:42:56.294 2012-2012/com.example.nat.morseflasher I/Choreographer﹕ Skipped 47 frames! The application may be doing too much work on its main thread.
08-09 15:43:07.725 2012-2012/com.example.nat.morseflasher I/Choreographer﹕ Skipped 1159 frames! The application may be doing too much work on its main thread.
EDIT 2:
Ok, so I've implemented the code you provided into mine, although I changed some things so it fits with how I want it.
When I try to run this, the log shows the "Starting morse code flasher..." message, but it never gets to the printing letter or symbol parts. When I tap the ready button, nothing changes at all, and it just goes back to the main activity as defined in the onPostExecute function.
Here's the XML:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#0099cc"
tools:context="com.example.nat.morseflasher.FlashActivity">
<!-- The primary full-screen view. This can be replaced with whatever view
is needed to present your content, e.g. VideoView, SurfaceView,
TextureView, etc. -->
<TextView android:id="#+id/fullscreen_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true"
android:textColor="#33b5e5"
android:textStyle="bold"
android:textSize="50sp"
android:gravity="center"
android:text="#string/dummy_content"
android:background="#000000" />
<!-- This FrameLayout insets its children based on system windows using
android:fitsSystemWindows. -->
<FrameLayout android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:background="#000000"
android:id="#+id/frameLayout">
<LinearLayout android:id="#+id/fullscreen_content_controls"
style="?metaButtonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:background="#color/black_overlay"
android:orientation="horizontal"
tools:ignore="UselessParent">
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="-"
android:id="#+id/labelCurrent"
android:layout_gravity="center"
android:layout_marginBottom="40dp"
android:textSize="100sp"
android:textColor="#ffffff" />
<TextView
android:layout_width="fill_parent"
android:layout_height="60dp"
android:textAppearance="?android:attr/textAppearanceSmall"
android:text="Ready?"
android:id="#+id/labelTotal"
android:layout_gravity="center"
android:layout_marginTop="40dp"
android:textSize="25sp"
android:gravity="center_horizontal"
android:textIsSelectable="false"
android:textColor="#ffffff" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/tapToGo"
android:id="#+id/buttonGo"
android:layout_gravity="center_horizontal|bottom"
android:layout_marginBottom="35dp"
android:onClick="doMorse" />
</FrameLayout>
</FrameLayout>
Here's the java:
package com.example.nat.morseflasher;
import com.example.nat.morseflasher.util.SystemUiHider;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.TextView;
/**
* An example full-screen activity that shows and hides the system UI (i.e.
* status bar and navigation/system bar) with user interaction.
*
* #see SystemUiHider
*/
public class FlashActivity extends Activity {
/**
* Whether or not the system UI should be auto-hidden after
* {#link #AUTO_HIDE_DELAY_MILLIS} milliseconds.
*/
private static final boolean AUTO_HIDE = true;
/**
* If {#link #AUTO_HIDE} is set, the number of milliseconds to wait after
* user interaction before hiding the system UI.
*/
private static final int AUTO_HIDE_DELAY_MILLIS = 3000;
/**
* If set, will toggle the system UI visibility upon interaction. Otherwise,
* will show the system UI visibility upon interaction.
*/
private static final boolean TOGGLE_ON_CLICK = true;
/**
* The flags to pass to {#link SystemUiHider#getInstance}.
*/
private static final int HIDER_FLAGS = SystemUiHider.FLAG_HIDE_NAVIGATION;
/**
* The instance of the {#link SystemUiHider} for this activity.
*/
private SystemUiHider mSystemUiHider;
private final static String TAG = "FlashActivity";
private final static long TIME_UNIT = 250L;
private final static long ONE_SECOND = 1000L;
private final static long DOT_DELAY = TIME_UNIT;
private final static long DASH_DELAY = TIME_UNIT * 2;
private final static long INTRA_LETTER_DELAY = TIME_UNIT;
private final static long INTER_LETTER_DELAY = TIME_UNIT * 2;
private final static long INTER_WORD_DELAY = TIME_UNIT * 6;
Button goButton;
FrameLayout layoutFrame;
TextView currentLabel, totalLabel;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_flash);
final View controlsView = findViewById(R.id.fullscreen_content_controls);
final View contentView = findViewById(R.id.fullscreen_content);
// Set up an instance of SystemUiHider to control the system UI for
// this activity.
mSystemUiHider = SystemUiHider.getInstance(this, contentView, HIDER_FLAGS);
mSystemUiHider.setup();
mSystemUiHider
.setOnVisibilityChangeListener(new SystemUiHider.OnVisibilityChangeListener() {
// Cached values.
int mControlsHeight;
int mShortAnimTime;
#Override
#TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2)
public void onVisibilityChange(boolean visible) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) {
// If the ViewPropertyAnimator API is available
// (Honeycomb MR2 and later), use it to animate the
// in-layout UI controls at the bottom of the
// screen.
if (mControlsHeight == 0) {
mControlsHeight = controlsView.getHeight();
}
if (mShortAnimTime == 0) {
mShortAnimTime = getResources().getInteger(
android.R.integer.config_shortAnimTime);
}
controlsView.animate()
.translationY(visible ? 0 : mControlsHeight)
.setDuration(mShortAnimTime);
} else {
// If the ViewPropertyAnimator APIs aren't
// available, simply show or hide the in-layout UI
// controls.
controlsView.setVisibility(visible ? View.VISIBLE : View.GONE);
}
if (visible && AUTO_HIDE) {
// Schedule a hide().
delayedHide(AUTO_HIDE_DELAY_MILLIS);
}
}
});
// Set up the user interaction to manually show or hide the system UI.
contentView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
if (TOGGLE_ON_CLICK) {
mSystemUiHider.toggle();
} else {
mSystemUiHider.show();
}
}
});
goButton = (Button) findViewById(R.id.buttonGo);
layoutFrame = (FrameLayout) findViewById(R.id.frameLayout);
currentLabel = (TextView) findViewById(R.id.labelCurrent);
totalLabel = (TextView) findViewById(R.id.labelTotal);
// Upon interacting with UI controls, delay any scheduled hide()
// operations to prevent the jarring behavior of controls going away
// while interacting with the UI.
}
#Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
// Trigger the initial hide() shortly after the activity has been
// created, to briefly hint to the user that UI controls
// are available.
delayedHide(100);
}
/**
* Touch listener to use for in-layout UI controls to delay hiding the
* system UI. This is to prevent the jarring behavior of controls going away
* while interacting with activity UI.
*/
View.OnTouchListener mDelayHideTouchListener = new View.OnTouchListener() {
#Override
public boolean onTouch(View view, MotionEvent motionEvent) {
if (AUTO_HIDE) {
delayedHide(AUTO_HIDE_DELAY_MILLIS);
}
return false;
}
};
Handler mHideHandler = new Handler();
Runnable mHideRunnable = new Runnable() {
#Override
public void run() {
mSystemUiHider.hide();
}
};
/**
* Schedules a call to hide() in [delay] milliseconds, canceling any
* previously scheduled calls.
*/
private void delayedHide(int delayMillis) {
mHideHandler.removeCallbacks(mHideRunnable);
mHideHandler.postDelayed(mHideRunnable, delayMillis);
}
public void doMorse(View vw) {
goButton.setVisibility(View.GONE);
(new DoMorseFlashing()).execute();
}
private class DoMorseFlashing extends AsyncTask <String, Boolean, Void> {
SharedPreferences mPrefs = getSharedPreferences("MorseFlasher", Context.MODE_PRIVATE);
String msgStr, currentLetterMorse;
#Override
protected Void doInBackground(String... message) {
Log.v(TAG, "Starting Morse Code flasher...");
msgStr = mPrefs.getString("prevMsg", "");
String totalStr = "";
/*currentLabel.setText("3");
*doDelay(ONE_SECOND);
*currentLabel.setText("2");
*doDelay(ONE_SECOND);
*currentLabel.setText("1");
*doDelay(ONE_SECOND);
*/
for(int i=0; i<msgStr.length(); i++) {
if(msgStr.charAt(i)=='|') {
doDelay(INTER_WORD_DELAY);
}
else {
currentLetterMorse = convert(msgStr.charAt(i));
currentLabel.setText(msgStr.substring(i, i + 1));
totalStr += msgStr.substring(i, i + 1);
totalLabel.setText(totalStr);
Log.v(TAG, " flashing letter " + msgStr.charAt(i) + " with morse code " + currentLetterMorse + ":");
for (int j = 0; j < currentLetterMorse.length(); j++) {
Log.v(TAG, "flashing symbol " + currentLetterMorse.charAt(j) + ":");
flash(currentLetterMorse.charAt(j));
doDelay(INTRA_LETTER_DELAY);
}
doDelay(INTER_LETTER_DELAY);
}
}
return null;
}
protected void onPostExecute(Void result) {
// Set the button message back to "touch here" to indicate that were done flashing:
startActivity(new Intent(getApplicationContext(), MainActivity.class));
}
void flash(char letter) {
layoutFrame.setBackgroundColor(Color.WHITE);
currentLabel.setTextColor(Color.BLACK);
totalLabel.setTextColor(Color.BLACK);
switch (letter) {
case '.':
doDelay(DOT_DELAY);
case '-':
doDelay(DASH_DELAY);
}
layoutFrame.setBackgroundColor(Color.BLACK);
currentLabel.setTextColor(Color.WHITE);
totalLabel.setTextColor(Color.WHITE);
}
String convert(char letter){
switch (letter){
case 'a':
return ".-";
case 'b':
return "-…";
case 'c':
return "-.-.";
case 'd':
return "-..";
case 'e':
return ".";
case 'f':
return "..-.";
case 'g':
return "--.";
case 'h':
return "….";
case 'i':
return "..";
case 'j':
return ".---";
case 'k':
return "-.-";
case 'l':
return ".-..";
case 'm':
return "--";
case 'n':
return "-.";
case 'o':
return "---";
case 'p':
return ".--.";
case 'q':
return "--.-";
case 'r':
return ".-.";
case 's':
return "...";
case 't':
return "-";
case 'u':
return "..-";
case 'v':
return "...-";
case 'w':
return ".--";
case 'x':
return "-..-";
case 'y':
return "-.--";
case 'z':
return "--..";
case ' ':
return "|";
default:
return "|";
}
}
void doDelay(Long delay) {
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
// Ignore interruptions
}
}
}
}
And here's the LogCat:
08-10 18:30:43.856 2090-2090/com.example.nat.morseflasher D/gralloc_goldfish﹕ Emulator without GPU emulation detected.
08-10 18:30:46.146 2090-2106/com.example.nat.morseflasher D/dalvikvm﹕ GC_FOR_ALLOC freed 146K, 8% free 3192K/3448K, paused 31ms, total 39ms
08-10 18:30:50.816 2090-2090/com.example.nat.morseflasher I/Choreographer﹕ Skipped 464 frames! The application may be doing too much work on its main thread.
08-10 18:31:04.116 2090-2090/com.example.nat.morseflasher I/Choreographer﹕ Skipped 69 frames! The application may be doing too much work on its main thread.
08-10 18:31:06.486 2090-2090/com.example.nat.morseflasher I/Choreographer﹕ Skipped 62 frames! The application may be doing too much work on its main thread.
08-10 18:31:11.176 2090-2090/com.example.nat.morseflasher I/Choreographer﹕ Skipped 37 frames! The application may be doing too much work on its main thread.
08-10 18:31:11.937 2090-2106/com.example.nat.morseflasher V/FlashActivity﹕ Starting Morse Code flasher...
08-10 18:31:13.437 2090-2090/com.example.nat.morseflasher I/Choreographer﹕ Skipped 109 frames! The application may be doing too much work on its main thread.
EDIT 3:
Ok, so the earlier problem was due to the fact that it wasn't retrieving my string from SharedPreferences, so just to make sure everything else worked, I set the message string to "test" in the code. Now, the problem is that when it goes to change the colour of the screen, I get an error saying that only the main thread that created the view hierarchy can touch its views.
EDIT 4:
I've fixed the error as aforementioned by using runOnUiThread() when I go to change the screen and text colours, the flashing now works!
The LogCat messages are telling you that your app is spending too much time on the main thread and starving the GUI of processing time.
If all you want to do is flash the screen, you could use a simple AsyncTask rather than all the complexity of your SystemUiHider.
Here's some code that shows how a background AsyncTask could be used using simple thread-sleeps to handle the timing. Since it's a background task, the sleeping won't interfere with the main GUI thread.
The main feature is that publishProgess() is called periodically to turn the flasher on or off. Note you can't make GUI changes from within the doInBackground() method stack, but can within the onProgessUpdate() and onPostExecute() ones. I used the same button for both starting and showing the flashing so that I could demonstrate using the onPostExecute() method to reset the button text.
You'll obviously need to add Morse code strings for the other letters and probably tweak the timing values to get things to look right.
MainActivity.java
package com.example.com;
import android.app.Activity;
import android.graphics.Color;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
public class MainActivity extends Activity {
// String for LogCat documentation
private final static String TAG = "MainActivity";
// These define the lengths for the dots, dashes, and times in between:
private final static long TIME_UNIT = 250L;
private final static long DOT_DELAY = TIME_UNIT * 2;
private final static long DASH_DELAY = TIME_UNIT * 3;
private final static long INTRA_LETTER_DELAY = TIME_UNIT;
private final static long INTER_LETTER_DELAY = TIME_UNIT * 3;
private final static long INTER_WORD_DELAY = TIME_UNIT * 7;
private Button flasher = null;
private EditText message = null;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
flasher = (Button) findViewById(R.id.flasher);
message = (EditText) findViewById(R.id.message);
}
public void doMorse(View vw) {
String messageStr = message.getText().toString();
if (messageStr.length() > 0) {
// This creates and starts a background task to do the flashing:
(new DoMorseFlashing()).execute(messageStr);
// Set the button message to indicate that we're flashing a message:
flasher.setText(R.string.flashing_message);
}
}
private class DoMorseFlashing extends AsyncTask <String, Boolean, Void> {
#Override
protected Void doInBackground(String... message) {
Log.v(TAG, "Starting Morse Code flasher...");
for (char letter : message[0].toCharArray()) {
if (letter == ' ')
doDelay(INTER_WORD_DELAY);
else
showLetter(letter);
}
return null;
}
protected void onProgressUpdate(Boolean... flasherOn) {
if (flasherOn[0])
flasher.setBackgroundColor(Color.BLUE);
else
flasher.setBackgroundColor(Color.WHITE);
}
protected void onPostExecute(Void result) {
// Set the button message back to "touch here" to indicate that we're done flashing:
flasher.setText(R.string.touch_here);
}
void showLetter(char letter) {
Log.v(TAG, "Flashing code for " + letter);
switch (letter) {
case 'A': showMorse(".-"); break;
case 'B': showMorse("-..."); break;
case 'C': showMorse("-.-."); break;
// cases for the other letters...
default: /* skip character */
}
doDelay(INTER_LETTER_DELAY);
}
void showMorse(String morse) {
Log.v(TAG, "Flashing code " + morse);
for (char dotDash: morse.toCharArray()) {
Log.v(TAG, "Flashing " + dotDash);
publishProgress(true);
if (dotDash == '.')
doDelay(DOT_DELAY);
else
doDelay(DASH_DELAY);
doDelay(INTRA_LETTER_DELAY);
publishProgress(false);
}
}
void doDelay(Long delay) {
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
// Ignore interruptions
}
}
}
}
activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<EditText
android:id="#+id/message"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:inputType="textCapCharacters"
android:hint="#string/enter_message" />
<Button
android:id="#+id/flasher"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="top|center"
android:background="#android:color/background_light"
android:hint="#string/touch_here"
android:onClick="doMorse" />
</LinearLayout>
strings.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Morse Flasher</string>
<string name="enter_message">Enter message</string>
<string name="touch_here">Touch here to flash message</string>
<string name="flashing_message">Flashing message</string>
</resources>
Related
Lets say, in an Android app, we want to have the ability to temporarily and reliably ignore all user touches at any moment.
From the research I have done on stack-overflow as well as here, here, and here, the agreed-upon solution seems to be something like this:
(Code of MainActivity.java):
// returning true should mean touches are ignored/blocked
#Override
public boolean dispatchTouchEvent(MotionEvent pEvent) {
if (disableTouches) {
return true;
} else {
return super.dispatchTouchEvent(pEvent);
}
}
However, when we introduce the Android Monkey Exerciser Tool and send touch events to the app at a rapid rate, it becomes apparent that pigs begin to fly at the quantum level -- we can get calls to onClick() even after/during times where "blockTouches" has been set to true.
MY QUESTION IS: Why is that? -- Is this a normal Android behavior, or did I make a mistake in my code? :)
Note: I have already ruled out the possibility of onClick() being called by user input other than touches (and therefore being uncontrolled by the onDispatchTouchEvent() method)... by adding "—-pct-touch 100" to the monkey command.
Here is the code I am using for this test:
MainActivity:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
View rootView; // turns black when "touch rejection" is in progress
View allowedButton;
View notAllowedButton;
// Used to decide whether to process touch events.
// Set true temporarily when notAllowedButton is clicked.
boolean touchRejectionAnimationInProgress = false;
int errorCount = 0; // counting "unexpected/impossible" click calls
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
rootView = findViewById(R.id.rootView);
allowedButton = findViewById(R.id.allowedButton);
notAllowedButton = findViewById(R.id.notAllowedButton);
allowedButton.setOnClickListener(this);
notAllowedButton.setOnClickListener(this);
allowedButton.setBackgroundColor(Color.GREEN);
notAllowedButton.setBackgroundColor(Color.RED);
}
// returning true should mean touches are ignored/blocked
#Override
public boolean dispatchTouchEvent(MotionEvent pEvent) {
if (touchRejectionAnimationInProgress) {
Log.i("XXX", "touch rejected in dispatchTouchevent()");
return true;
} else {
return super.dispatchTouchEvent(pEvent);
}
}
#Override
public void onClick(View viewThatWasClicked){
Log.i("XXX", "onClick() called. View clicked: " + viewThatWasClicked.getTag());
//checking for unexpected/"impossible"(?) calls to this method
if (touchRejectionAnimationInProgress) {
Log.i("XXX!", "IMPOSSIBLE(?) call to onClick() detected.");
errorCount ++;
Log.i("XXX!", "Number of unexpected clicks: " + errorCount);
return;
} // else proceed...
if (viewThatWasClicked == allowedButton) {
// Irrelevant
} else if (viewThatWasClicked == notAllowedButton) {
// user did something that is not allowed.
touchRejectionAnimation();
}
}
// When the user clicks on something "illegal,"
// all user input is ignored temporarily for 200 ms.
// (arbitrary choice of duration, but smaller is better for testing)
private void touchRejectionAnimation() {
Log.i("XXX", "touchRejectionAnimation() called.");
touchRejectionAnimationInProgress = true;
rootView.setBackgroundColor(Color.BLACK);
// for logging/debugging purposes...
final String rejectionID = (new Random().nextInt() % 9999999) + "";
Log.i("XXX", "rejection : " + rejectionID + " started.");
Thread thread = new Thread(new Runnable() {
#Override
public void run() {
try { Thread.sleep(200); } catch (Exception e) {
Log.e("XXX", "exception in touchRejection() BG thread!");
}
runOnUiThread(new Runnable() {
#Override
public void run() {
Log.i("XXX", "rejection " + rejectionID + " ending");
rootView.setBackgroundColor(Color.WHITE);
touchRejectionAnimationInProgress = false;
}
});
}
});
thread.start();
}
}
layout.xml:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/rootView"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<View
android:id="#+id/allowedButton"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="32dp"
android:layout_marginBottom="32dp"
android:tag="allowedButton"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="#+id/notAllowedButton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="#+id/notAllowedButton"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="32dp"
android:layout_marginBottom="32dp"
android:tag="view2"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="#+id/allowedButton"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
If you don't want your onClick() to be triggered on any view click.
Following are the steps which need to take care.
Create custom viewGroup eg: MyConstraintLayout and add all child
inside it.
Override onInterceptTouchEvent(MotionEvent ev) and return it has true.
public class MyConstraintLayout extends ConstraintLayout {
private boolean mIsViewsTouchable;
public ParentView(Context context) {
super(context);
}
public ParentView(Context context, AttributeSet attrs) {
super(context, attrs);
inflate(context, R.layout.custom_view, this);
}
public ParentView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setViewsTouchable(boolean isViewTouchable) {
mIsViewsTouchable = isViewTouchable;
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mIsViewsTouchable;
}
}
Note: Use setViewsTouchable() method as per your requirement, If you pass the parameter as true all views are not clickable if false your views will be clickable.
I'm a beginner with Android, so please be kind if this is a stupid question.
I'm trying to dynamically update four TextViews. Whenever I try to update them, the program crashes.
I don't understand the explanation "Only the original thread that created a view hierarchy can touch its views."
Here is my class:
Globals g = new Globals();
String l1 = "";
String l2 = "";
String l3 = "";
String l4 = "";
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_game);
}
#Override
public void onStart() {
super.onStart();
//View view = getLayoutInflater().inflate(R.layout.activity_game, null);
run ();
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_game, menu);
return true;
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
private void nextLine (String s, int pauseTime, TextView first, TextView second, TextView third, TextView fourth)
{
l4 = l3;
l3 = l2;
l2 = l1;
l1 = s;
first.setText (l1);
second.setText (l2);
third.setText (l3);
fourth.setText(l4);
try
{
Thread.sleep (pauseTime);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
#Override
public void onPause ()
{
super.onPause ();
}
#Override
public void run () {
Thread thread = new Thread() {
#Override
public void run() {
TextView first = (TextView) findViewById(R.id.botLine);
TextView second = (TextView) findViewById(R.id.secondLine);
TextView third = (TextView) findViewById(R.id.thirdLine);
TextView fourth = (TextView) findViewById(R.id.fourthLine);
first.setTypeface(Typeface.MONOSPACE);
second.setTypeface(Typeface.MONOSPACE);
third.setTypeface(Typeface.MONOSPACE);
fourth.setTypeface(Typeface.MONOSPACE);
first.setTextSize((float) g.getTextSizeInt());
second.setTextSize((float) g.getTextSizeInt());
third.setTextSize((float) g.getTextSizeInt());
fourth.setTextSize((float) g.getTextSizeInt());
nextLine("1", 1000, first, second, third, fourth);
nextLine("2", 1000, first, second, third, fourth);
}
};
thread.start();
}
...and this is my LogCat:
08-05 02:33:34.129 14823-14854/com.mycompany.TestApp E/AndroidRuntime﹕ FATAL EXCEPTION: Thread-190
Process: com.mycompany.TestApp, PID: 14823
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6357)
at android.view.ViewRootImpl.invalidateChildInParent(ViewRootImpl.java:909)
at android.view.ViewGroup.invalidateChild(ViewGroup.java:4690)
at android.view.View.invalidateInternal(View.java:11801)
at android.view.View.invalidate(View.java:11765)
at android.view.View.invalidate(View.java:11749)
at android.widget.TextView.checkForRelayout(TextView.java:6858)
at android.widget.TextView.setText(TextView.java:4057)
at android.widget.TextView.setText(TextView.java:3915)
at android.widget.TextView.setText(TextView.java:3890)
at com.mycompany.TestApp.Game.nextLine(Game.java:64)
at com.mycompany.TestApp.Game.access$000(Game.java:13)
at com.mycompany.TestApp.Game$1.run(Game.java:106)
08-05 02:33:34.266 14823-14839/com.mycompany.TestApp W/EGL_emulation﹕ eglSurfaceAttrib not implemented
08-05 02:33:34.266 14823-14839/com.mycompany.TestApp W/OpenGLRenderer﹕ Failed to set EGL_SWAP_BEHAVIOR on surface 0xb3f1ff20, error=EGL_SUCCESS
...and the XML code:
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:id="#+id/botLine"
android:layout_marginBottom="70dp"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" />
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:id="#+id/secondLine"
android:layout_marginBottom="70dp"
android:layout_above="#+id/botLine"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" />
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:id="#+id/thirdLine"
android:layout_marginBottom="70dp"
android:layout_above="#+id/secondLine"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" />
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:id="#+id/fourthLine"
android:layout_marginBottom="70dp"
android:layout_above="#+id/thirdLine"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" />
</RelativeLayout>
Program is getting crashed because you are trying to update the text on TextView from a non UI Thread.
replace your run() method with this:
public void run () {
Handler handler = new Handler(Looper.getMainLooper);
handler.post(new Runnable(){
#Override
public void run(){
TextView first = (TextView) findViewById(R.id.botLine);
TextView second = (TextView) findViewById(R.id.secondLine);
TextView third = (TextView) findViewById(R.id.thirdLine);
TextView fourth = (TextView) findViewById(R.id.fourthLine);
first.setTypeface(Typeface.MONOSPACE);
second.setTypeface(Typeface.MONOSPACE);
third.setTypeface(Typeface.MONOSPACE);
fourth.setTypeface(Typeface.MONOSPACE);
first.setTextSize((float) g.getTextSizeInt());
second.setTextSize((float) g.getTextSizeInt());
third.setTextSize((float) g.getTextSizeInt());
fourth.setTextSize((float) g.getTextSizeInt());
nextLine("1", 1000, first, second, third, fourth);
nextLine("2", 1000, first, second, third, fourth);
}
});
}
You can't change the UI from another thread, you should use a method that deals with the main thread, for example you can try ASYNCtask to do what you want in the background then change the textviews in onPostExecute()
here you are a link to it:
http://developer.android.com/reference/android/os/AsyncTask.html
Update:
example for AsyncTask
class Name extends AsyncTask<Void, Void, Boolean>{
//you can add variables here to be public in the class
// also you can add a constructor to the class
#Override
protected void onPreExecute() {
super.onPreExecute();
// here the code that you wanna do before starting the thread
// you can delete this method if you want
}
#Override
protected Boolean doInBackground(Void... parms) {
// this is the method that will do the word away from the main thread
// it must exist in the AsyncTask
// this method will return boolean but you should check if it's null or not
}
#Override
protected void onPostExecute(Boolean result) {
super.onPostExecute(result);
// in this method you can deal with UI
}
}
I am currently using a switch in my Android application as sort of options in my settings class. However, when I change the state of the switch, from say false to true, and go back to the menu, and then go back to the settings activity, the state of the switch goes back to the default value. Is there a key line that basically saves the switch's state so that when a user returns to it, the state is the same? I don't think this question requires code, but if you think it's helpful, please let me know and I'll attach it. Thanks!
here is my activity code:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="#dimen/activity_vertical_margin"
android:paddingLeft="#dimen/activity_horizontal_margin"
android:paddingRight="#dimen/activity_horizontal_margin"
android:paddingTop="#dimen/activity_vertical_margin"
tools:context="com.example.speedytext.Options" >
<TextView
android:id="#+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Switch
android:id="#+id/switch1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="#+id/textView1"
android:layout_marginLeft="46dp"
android:layout_marginTop="48dp"
android:layout_toRightOf="#+id/textView1"
android:text="Switch" />
<Switch
android:id="#+id/switch2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignRight="#+id/switch1"
android:layout_below="#+id/switch1"
android:layout_marginTop="66dp"
android:text="Switch" />
</RelativeLayout>
here is my java code:
import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.Switch;
public class Options extends ActionBarActivity {
private static Switch ding;
private Switch countdown;
public static boolean isDingChecked;
public static boolean isCountdownChecked;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_options);
ding = (Switch) findViewById(R.id.switch1);
ding.setOnCheckedChangeListener(new OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
// do something, the isChecked will be
// true if the switch is in the On position
isDingChecked = isChecked;
System.out.println(isDingChecked);
System.out.println(isDingChecked());
}
});
countdown = (Switch) findViewById(R.id.switch2);
countdown.setOnCheckedChangeListener(new OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
// do something, the isChecked will be
// true if the switch is in the On position
isCountdownChecked = isChecked;
System.out.println(isCountDownChecked());
}
});
isDingChecked = ding.isChecked();
isCountdownChecked= countdown.isChecked();
}
public static boolean isDingChecked()
{
return isDingChecked;
}
public static boolean isCountDownChecked()
{
return isCountdownChecked;
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.options, menu);
return true;
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
}
In onCreate(), call setChecked() with your stored "is checked" value to set the default to whatever was stored previously.
Also, consider storing the settings to e.g. shared preferences. static variables do not persist over application relaunches and often make your code harder to maintain.
Shared Preference Class
public class AppPreferences {
private SharedPreferences appSharedPrefs;
private Editor prefsEditor;
public AppPreferences(Context context, String Preferncename) {
this.appSharedPrefs = context.getSharedPreferences(Preferncename,
Activity.MODE_PRIVATE);
this.prefsEditor = appSharedPrefs.edit();
}
/****
*
* getdata() get the value from the preference
*
* */
public String getData(String key) {
return appSharedPrefs.getString(key, "");
}
/****
*
* SaveData() save the value to the preference
*
* */
public void SaveData(String text, String Tag) {
prefsEditor.putString(Tag, text);
prefsEditor.commit();
}
public void clear()
{
prefsEditor.clear();
prefsEditor.commit();
}
}
Usage of Shared Preference in activity
AppPreferences appPref;
appPref = new AppPreferences(getApplicationContext(), "PREFS");
To save the state
When the state of the switch is changed save the state true or false to a tag in shared preference
You can use this line to save the state
appPref.SaveData("Value", "Tag");
so it will be like this for checked state appPref.SaveData("true", "state");
and appPref.SaveData("false", "state"); for unchecked state
in the oncreate of your activity get the value from the shared preference
using
appPref.getData("state");
if(appPref.getData("state").equals("true"))
{
//set ur switch button to checked state
}
else if(appPref.getData("state").equals("false"))
{
//set ur switch button to unchecked state
}
then check the result string for true or false
and set the switch accordingly
Every time I try to run the application with the list view in a location in crashes or when i get it working the list view doesnt show up iv tried changing where the list view comes ie after the first frame layout under the fullscreen content and after the frame layout full screen content controls and still cant get it working
here is the active view xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#0099cc"
tools:context=".ActiveCheatsView" >
<TextView
android:id="#+id/fullscreen_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:keepScreenOn="true"
android:text="#string/ActiveCheats"
android:textColor="#000000"
android:textSize="50sp"
android:textStyle="bold" />
<!--
This FrameLayout insets its children based on system windows using
android:fitsSystemWindows.
-->
<ListView
android:id="#+id/Games"
android:layout_width="fill_parent"
android:layout_height="410dp" >
</ListView>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true" >
<LinearLayout
android:id="#+id/fullscreen_content_controls"
style="?buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:background="#color/black_overlay"
android:orientation="horizontal"
tools:ignore="UselessParent" >
<Button
android:id="#+id/Button01"
style="?buttonBarButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="#string/Search" />
</LinearLayout>
</FrameLayout>
and here is the java view
package com.example.activecheats;
import com.example.activecheats.util.SystemUiHider;
import android.annotation.TargetApi;
import android.app.Activity;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ListView;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
/**
* An example full-screen activity that shows and hides the system UI (i.e.
* status bar and navigation/system bar) with user interaction.
*
* #see SystemUiHider
*/
public class ActiveCheatsView extends Activity {
/**
* Whether or not the system UI should be auto-hidden after
* {#link #AUTO_HIDE_DELAY_MILLIS} milliseconds.
*/
private static final boolean AUTO_HIDE = true;
/**
* If {#link #AUTO_HIDE} is set, the number of milliseconds to wait after
* user interaction before hiding the system UI.
*/
private static final int AUTO_HIDE_DELAY_MILLIS = 3000;
/**
* If set, will toggle the system UI visibility upon interaction. Otherwise,
* will show the system UI visibility upon interaction.
*/
private static final boolean TOGGLE_ON_CLICK = true;
/**
* The flags to pass to {#link SystemUiHider#getInstance}.
*/
private static final int HIDER_FLAGS = SystemUiHider.FLAG_HIDE_NAVIGATION;
/**
* The instance of the {#link SystemUiHider} for this activity.
*/
private SystemUiHider mSystemUiHider;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_active_cheats_view);
final View controlsView = findViewById(R.id.fullscreen_content_controls);
final View contentView = findViewById(R.id.fullscreen_content);
// Set up an instance of SystemUiHider to control the system UI for
// this activity.
mSystemUiHider = SystemUiHider.getInstance(this, contentView,
HIDER_FLAGS);
mSystemUiHider.setup();
mSystemUiHider
.setOnVisibilityChangeListener(new SystemUiHider.OnVisibilityChangeListener() {
// Cached values.
int mControlsHeight;
int mShortAnimTime;
#Override
#TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2)
public void onVisibilityChange(boolean visible) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) {
// If the ViewPropertyAnimator API is available
// (Honeycomb MR2 and later), use it to animate the
// in-layout UI controls at the bottom of the
// screen.
if (mControlsHeight == 0) {
mControlsHeight = controlsView.getHeight();
}
if (mShortAnimTime == 0) {
mShortAnimTime = getResources().getInteger(
android.R.integer.config_shortAnimTime);
}
controlsView
.animate()
.translationY(visible ? 0 : mControlsHeight)
.setDuration(mShortAnimTime);
} else {
// If the ViewPropertyAnimator APIs aren't
// available, simply show or hide the in-layout UI
// controls.
controlsView.setVisibility(visible ? View.VISIBLE
: View.GONE);
}
if (visible && AUTO_HIDE) {
// Schedule a hide().
delayedHide(AUTO_HIDE_DELAY_MILLIS);
}
}
});
// Set up the user interaction to manually show or hide the system UI.
contentView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
if (TOGGLE_ON_CLICK) {
mSystemUiHider.toggle();
} else {
mSystemUiHider.show();
}
}
});
// Upon interacting with UI controls, delay any scheduled hide()
// operations to prevent the jarring behavior of controls going away
// while interacting with the UI.
findViewById(R.id.Button01).setOnTouchListener(
mDelayHideTouchListener);
}
#Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
// Trigger the initial hide() shortly after the activity has been
// created, to briefly hint to the user that UI controls
// are available.
delayedHide(100);
}
/**
* Touch listener to use for in-layout UI controls to delay hiding the
* system UI. This is to prevent the jarring behavior of controls going away
* while interacting with activity UI.
*/
View.OnTouchListener mDelayHideTouchListener = new View.OnTouchListener() {
#Override
public boolean onTouch(View view, MotionEvent motionEvent) {
if (AUTO_HIDE) {
delayedHide(AUTO_HIDE_DELAY_MILLIS);
}
return false;
}
};
Handler mHideHandler = new Handler();
Runnable mHideRunnable = new Runnable() {
#Override
public void run() {
mSystemUiHider.hide();
}
};
/**
* Schedules a call to hide() in [delay] milliseconds, canceling any
* previously scheduled calls.
*/
private void delayedHide(int delayMillis) {
mHideHandler.removeCallbacks(mHideRunnable);
mHideHandler.postDelayed(mHideRunnable, delayMillis);
}
private List<Games> gamesList= new ArrayList<Games>();
private List<Games> gamesList;
/** Called when the activity is first created. */
public void onCreate1(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Set the View layer
setContentView(R.layout.activity_active_cheats_view);
setTitle("ActiveCheats");
// Create Parser for games.xml
GamesParser GamesParser = new GamesParser();
InputStream inputStream = getResources().openRawResource(
R.layout.games);
// Parse the inputstream
GamesParser.parse(inputStream);
setGamesList(com.example.activecheats.GamesParser.getList());
// Create a customized ArrayAdapter
GamesArrayAdapter adapter = new GamesArrayAdapter(
getApplicationContext(), R.layout.games_listitem, countryList);
// Get reference to ListView holder
ListView lv = (ListView) this.findViewById(R.id.Games);
// Set the ListView adapter
lv.setAdapter(adapter);
}
public List<Games> getGamesList() {
return gamesList;
}
public void setGamesList(List<Games> gamesList) {
this.gamesList = gamesList;
}
}
<ListView
android:id="#android:id/list"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>
try this in your xml file and in your Mianactivity should extend ListActivity
I'd like to have a TextView display text, and when you click/longclick on it, a textbox should "show up" and allow editing of said text. When you're done editing (onkey enter i suppose) it should revert back to a textview with the updated text...
I'm wondering if it's feasable to implement such a widget or should I hack a workaround? Tips and suggestions are very welcome.
If you need further idea of what I mean, just go to your e.g. (windows) skype profile and see for yourself.
EDIT:
Clarification: I'm specifically asking for a widget or such which is a textview until clicked on, then transforms to an edittext containing the same text; once done editing it transforms back to a textview representing the new changed text. Thats what i mean by "edittext on demand widget".
But I'm hoping to get something better than
public class Widget {
TextView text;
EditText edit;
String textToRepresent;
//...
}
You have a few different options here.
First you will have to register an onClick or onLongClick to the TextView that you want to make interactive. Just make sure that the user knows it's clickable
Then have your onClick function start a DialogFragment. I like to create show functions. Note that you can use the support libraries here to make your app backwards compatible.
private void showDialog() {
MyDialogFragment dialog = new MyDialogFragment();
dialog.show(getSupportFragmentManager(), "dialog");
}
The DialogFragment is pretty straight forward. In your onCreateView you'll inflate the View that you'll want to display to the user. You can alternatively wrap it with a simple AlertDialogBuilder if you don't want to go custom.
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.your_dialog_layout);
mTitleEditText = (TextView) view.findViewById(R.id.title);
mTitleEditText.setOnClickListener(this);
return view;
}
After your findViewByIds set your onClickListeners.
The last thing you have to take care of is getting data back into your original TextView.
You can do this by creating a public method in your Activity that you can call from inside of your DialogFragment. Something like this
#Override
public void onClick(View v) {
int clickedId = v.getId();
if (clickedId == mDoneButton.getId()) {
MyActivity activity = (MyActivity)getActivity();
mTitle = mTitleEditText.getText().toString();
activity.setText(mTitle);
dismiss();
}
}
I would recommend using a DialogFragment because it will handle your life cycle nicely.
However, another option would be to create a new Activity themed to be a dialog
<activity android:theme="#android:style/Theme.Dialog" />
Then you can startActivityForResult to display your dialog and then capture your results in onActivityResult
Here is my solution. I just give you the basic one. Create a TextView in front of EditText and two Button OK,Cancel (You can change to ImageButton like Skype). Change the visiblity of two view. The code is so simple without comment. You can add some null checking according your logic.
public class CompoundTextView extends RelativeLayout implements OnClickListener {
private EditText edt;
private TextView txt;
RelativeLayout layout;
public SkypeTextView(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
}
#Override
protected void onFinishInflate() {
super.onFinishInflate();
edt = (EditText) findViewById(R.id.edt);
txt = (TextView) findViewById(R.id.txt_name);
layout = (RelativeLayout) findViewById(R.id.layout);
Button ok = (Button) findViewById(R.id.ok_btn);
Button cancel = (Button) findViewById(R.id.cancel_btn);
ok.setOnClickListener(this);
cancel.setOnClickListener(this);
txt.setOnClickListener(this);
}
public void onClick(View v) {
// TODO Auto-generated method stub
switch (v.getId()) {
case R.id.ok_btn:
String editString = edt.getText().toString();
txt.setText(editString);
layout.setVisibility(View.INVISIBLE);
txt.setVisibility(View.VISIBLE);
break;
case R.id.cancel_btn:
layout.setVisibility(View.INVISIBLE);
txt.setVisibility(View.VISIBLE);
break;
case R.id.txt_name:
txt.setVisibility(View.INVISIBLE);
layout.setVisibility(View.VISIBLE);
break;
}
}
}
Create a XML skypetextview. You can customize font and background to make it's prettier.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<TextView
android:id="#+id/txt_name"
android:layout_width="fill_parent"
android:layout_height="100dp"
android:textColor="#FFFFFF"
android:textSize="14sp"
android:background="#ff0000" />
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="invisible"
android:id="#+id/layout" >
<EditText
android:id="#+id/edt"
android:layout_width="270dp"
android:layout_height="100dp" />
<Button
android:id="#+id/ok_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="#id/edt"
android:text="OK" />
<Button
android:id="#+id/cancel_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="#id/ok_btn"
android:layout_toRightOf="#id/edt"
android:text="Cancel" />
</RelativeLayout>
</RelativeLayout>
add (or include) this view to the layout you want.
Example :
public class TestActivity extends Activity {
SkypeTextView test;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LayoutInflater inflate = getLayoutInflater();
test = (SkypeTextView ) inflate.inflate(R.layout.compound_text_view,
null);
setContentView(test);
}
PS: i forgot. You should add some underline format for your textview in order to make user notice it clickable
Let a EditText change its background based on its state(Editable or Frozen). Set a background selector that does this.
Use this selector xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:state_focused="true" android:drawable="#android:drawable/edit_text"/>
<item android:drawable="#android:drawable/screen_background_light_transparent"/>
</selector>
Like I said on thursday... Yul was pretty close but not quite close. He did have a general same idea but (theoretically) rushed into code too early ;)
The TextBoxOnDemand code supplied below is production-ready. The idea is similar to what I wanted to avoid in the OP and what Yul suggested, but with optimal implementation (using a ViewSwitcher instead of a RelativeLayout for instance)
I gathered the resources needed for this in the following articles:
Creating custom view from xml
Declaring a custom android UI element using XML
Defining custom attrs
How to pass custom component parameters in java and xml
http://kevindion.com/2011/01/custom-xml-attributes-for-android-widgets/
and decided to post them here because the official Google "training" docs are useless and are either obsolete (deprecated) or do not cover what I needed. I hope you don't mind me claiming my own bounty, but this is the solution I wanted (and expected, ergo the bounty).
I guess the code will have to do ;)
TextBoxOnDemand.java:
package com.skype.widget;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.text.SpannableString;
import android.text.style.UnderlineSpan;
import android.text.util.Linkify;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnFocusChangeListener;
import android.view.View.OnHoverListener;
import android.view.View.OnLongClickListener;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
import android.widget.ViewSwitcher;
import com.skype.ref.R;
import com.skype.ref.RemoteKeys;
public class TextBoxOnDemand extends ViewSwitcher implements OnClickListener, OnLongClickListener, OnFocusChangeListener, OnHoverListener,
OnEditorActionListener
{
public static final String LOGTAG = "TextBoxOnDemand";
private View btmGuard;
private ImageButton cancel, accept;
private EditText editor;
private RelativeLayout editorLayout;
private TextView face;
private String hint = new String();
private boolean inEditMode = false; //normally this is in textview mode
private boolean inputReady = false;
private String ourData = new String();
private String prefillData = new String();
private String tag = new String(); //usually tag is empty.
private View topGuard;
private int autoLinkMask;// = Linkify.EMAIL_ADDRESSES; //Linkify.ALL;
private ColorStateList textColor, hintColor = null;
public TextBoxOnDemand(Context context)
{
super(context);
build(context);
setEditable(false); //init
}
public TextBoxOnDemand(Context context, AttributeSet attrs)
{
super(context, attrs);
build(context);
init(context, attrs);
setEditable(false); //init
}
public String getPrefillData()
{
return prefillData;
}
public String getTag()
{
return tag;
}
public String getText()
{
Log.d(LOGTAG, "getText() returning '" + ourData + "'");
return ourData;
}
public boolean hasPrefillData()
{
return prefillData.isEmpty();
}
public boolean isEditable()
{
Log.d(LOGTAG, "isEditable() returning " + inEditMode);
return inEditMode;
}
#Override
public void onClick(View v)
{
Log.d(LOGTAG, "onClick(" + v + ")");
if (inEditMode)
{
if (v.equals(accept))
{
if (editor.getEditableText().length() == 0 || editor.getEditableText().length() > 5)
ourData = editor.getEditableText().toString();
setEditable(false);
} else if (v.equals(cancel))
{
setEditable(false);
}
}
}
#Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event)
{
// Log.d(LOGTAG, "onEditorAction(" + v + ", " + actionId + ", " + event + ") fired!");
Log.d(LOGTAG, "onEditorAction() fired, inputReady = " + inputReady);
if (editor.getEditableText().length() > 0 && editor.getEditableText().length() < (prefillData.length() + 2)) return true; //the user needs to enter something
if (inputReady && (event.getKeyCode() == RemoteKeys.ENTER.keycode() || event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) //always is
{
if (editor.getEditableText().length() > prefillData.length() || editor.getEditableText().length() == 0)
ourData = editor.getEditableText().toString();
setEditable(false);
return false;
}
if ((editor.getEditableText().toString().compareToIgnoreCase(ourData) == 0 || editor.getEditableText().toString()
.compareToIgnoreCase(prefillData) == 0)
&& !inputReady) //means we didn't just keep on holding enter
return true;
else
inputReady = true;
return true;
}
#Override
public void onFocusChange(View v, boolean hasFocus)
{
Log.d(LOGTAG, "onFocusChange(" + v + ", " + hasFocus + ")\tinEditMode = " + inEditMode);
if (inEditMode)
{
if (hasFocus && (v.equals(topGuard) || v.equals(btmGuard)))
{
setEditable(false);
requestFocus();
}
if (hasFocus && (v.equals(editor) || v.equals(accept) || v.equals(cancel)))
{
//do nothing, you should be able to browse freely here
if (ourData.isEmpty() && editor.getEditableText().length() < prefillData.length())
{
Log.d(LOGTAG, "adding prefill, before = " + editor.getEditableText());
editor.setText("");
editor.append(prefillData);
Log.d(LOGTAG, "now is = " + editor.getEditableText());
}
}
} else
{
String text = (ourData.isEmpty()) ? hint : ourData;
ColorStateList color;
if (hintColor != null && ourData.isEmpty())
color = hintColor;
else
color = textColor;
face.setTextColor(color);
if (hasFocus)
{
SpannableString ss = new SpannableString(text);
ss.setSpan(new UnderlineSpan(), 0, text.length(), 0);
face.setText(ss);
} else
face.setText(text);
}
}
#Override
public boolean onHover(View v, MotionEvent event)
{
// Log.d(LOGTAG, "onHover()");
String text = (ourData.isEmpty()) ? hint : ourData;
ColorStateList color;
if (hintColor != null && ourData.isEmpty())
color = hintColor;
else
color = textColor;
face.setTextColor(color);
switch (event.getAction())
{
case MotionEvent.ACTION_HOVER_ENTER:
SpannableString ss = new SpannableString(text);
ss.setSpan(new UnderlineSpan(), 0, text.length(), 0);
face.setText(ss);
break;
case MotionEvent.ACTION_HOVER_EXIT:
face.setText(text);
break;
}
return true;
}
#Override
public boolean onLongClick(View v)
{
Log.d(LOGTAG, "onLongClick()\tinEditMode = " + inEditMode);
if (!inEditMode) //implies that getDisplayedChild() == 0, meaning the textview
{
setEditable(true);
return true;
} else
return false;
}
public void setEditable(boolean value)
{
Log.d(LOGTAG, "setEditable(" + value + ")");
inEditMode = value;
if (inEditMode)
{
//display the editorLayout
face.setOnLongClickListener(null);
face.setOnHoverListener(null);
face.setOnFocusChangeListener(null); //because of GC.
face.setOnClickListener(null);
face.setVisibility(View.GONE);
setDisplayedChild(1);
editorLayout.setVisibility(View.VISIBLE);
editor.setOnFocusChangeListener(this);
editor.setOnEditorActionListener(this);
cancel.setOnClickListener(this);
accept.setOnClickListener(this);
accept.setOnFocusChangeListener(this);
cancel.setOnFocusChangeListener(this);
} else
{
editor.setOnFocusChangeListener(null);
editor.setOnEditorActionListener(null);
cancel.setOnClickListener(null);
accept.setOnClickListener(null);
accept.setOnFocusChangeListener(null);
cancel.setOnFocusChangeListener(null);
editorLayout.setVisibility(View.GONE);
setDisplayedChild(0);
face.setVisibility(View.VISIBLE);
face.setOnLongClickListener(this);
face.setOnHoverListener(this);
face.setOnFocusChangeListener(this);
face.setOnClickListener(this);
face.setFocusable(true);
face.setFocusableInTouchMode(true);
}
updateViews();
}
#Override
public void setNextFocusDownId(int nextFocusDownId)
{
super.setNextFocusDownId(nextFocusDownId);
face.setNextFocusDownId(nextFocusDownId);
// editor.setNextFocusDownId(nextFocusDownId);
accept.setNextFocusDownId(nextFocusDownId);
cancel.setNextFocusDownId(nextFocusDownId);
}
#Override
public void setNextFocusForwardId(int nextFocusForwardId)
{
super.setNextFocusForwardId(nextFocusForwardId);
face.setNextFocusForwardId(nextFocusForwardId);
editor.setNextFocusForwardId(nextFocusForwardId);
}
#Override
public void setNextFocusLeftId(int nextFocusLeftId)
{
super.setNextFocusLeftId(nextFocusLeftId);
face.setNextFocusLeftId(nextFocusLeftId);
editor.setNextFocusLeftId(nextFocusLeftId);
}
#Override
public void setNextFocusRightId(int nextFocusRightId)
{
super.setNextFocusRightId(nextFocusRightId);
face.setNextFocusRightId(nextFocusRightId);
cancel.setNextFocusRightId(nextFocusRightId);
}
#Override
public void setNextFocusUpId(int nextFocusUpId)
{
super.setNextFocusUpId(nextFocusUpId);
face.setNextFocusUpId(nextFocusUpId);
// editor.setNextFocusUpId(nextFocusUpId);
accept.setNextFocusUpId(nextFocusUpId);
cancel.setNextFocusUpId(nextFocusUpId);
}
public void setPrefillData(String prefillData)
{
this.prefillData = new String(prefillData);
}
public String setTag()
{
return tag;
}
public void setText(String text)
{
Log.d(LOGTAG, "setText(" + text + ")");
ourData = text;
updateViews();
}
private void build(Context context)
{
Log.d(LOGTAG, "build()");
addView(View.inflate(context, R.layout.textboxondemand, null));
setFocusable(true);
setFocusableInTouchMode(true);
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
setOnFocusChangeListener(this);
setOnLongClickListener(this);
face = (TextView) findViewById(R.id.TBOD_textview);
editorLayout = (RelativeLayout) findViewById(R.id.TBOD_layout);
editor = (EditText) findViewById(R.id.TBOD_edittext);
accept = (ImageButton) findViewById(R.id.TBOD_accept);
cancel = (ImageButton) findViewById(R.id.TBOD_cancel);
topGuard = (View) findViewById(R.id.TBOD_top);
btmGuard = (View) findViewById(R.id.TBOD_bottom);
face.setFocusable(true);
face.setFocusableInTouchMode(true);
face.setOnLongClickListener(this);
face.setOnHoverListener(this);
face.setOnFocusChangeListener(this);
face.setOnClickListener(this);
editor.setOnFocusChangeListener(this);
editor.setOnEditorActionListener(this);
editor.setHint(hint);
editor.setFocusable(true);
editor.setFocusableInTouchMode(true);
accept.setOnClickListener(this);
accept.setOnFocusChangeListener(this);
accept.setFocusable(true);
cancel.setFocusable(true);
cancel.setOnFocusChangeListener(this);
cancel.setOnClickListener(this);
topGuard.setFocusable(true);
topGuard.setOnFocusChangeListener(this);
btmGuard.setFocusable(true);
btmGuard.setOnFocusChangeListener(this);
editor.setNextFocusRightId(R.id.TBOD_accept);
editor.setNextFocusDownId(R.id.TBOD_bottom);
editor.setNextFocusUpId(R.id.TBOD_top);
accept.setNextFocusLeftId(R.id.TBOD_edittext);
accept.setNextFocusRightId(R.id.TBOD_cancel);
cancel.setNextFocusLeftId(R.id.TBOD_accept);
}
private void init(Context context, AttributeSet attrs)
{
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TextBoxOnDemand);
//Use a
Log.d(LOGTAG, "init()");
if (a == null) Log.d(LOGTAG, "Did you include 'xmlns:app=\"http://schemas.android.com/apk/res-auto\"' in your root layout?");
final int N = a.getIndexCount();
for (int i = 0; i < N; ++i)
{
int attr = a.getIndex(i);
switch (attr)
{
case R.styleable.TextBoxOnDemand_android_hint:
hint = new String(a.getString(attr));
editor.setHint(a.getString(attr));
break;
case R.styleable.TextBoxOnDemand_android_text:
ourData = new String(a.getString(attr));
break;
case R.styleable.TextBoxOnDemand_android_inputType:
int inputType = a.getInt(attr, -1);
if (inputType != -1) editor.setInputType(inputType);
break;
case R.styleable.TextBoxOnDemand_android_textColor:
textColor = a.getColorStateList(attr);
face.setTextColor(textColor);
break;
case R.styleable.TextBoxOnDemand_android_linksClickable:
face.setLinksClickable(a.getBoolean(attr, true));
break;
case R.styleable.TextBoxOnDemand_android_textColorHint:
hintColor = a.getColorStateList(attr);
break;
case R.styleable.TextBoxOnDemand_android_autoLink:
autoLinkMask = a.getInt(attr, 0);
face.setAutoLinkMask(autoLinkMask);
break;
default:
Log.d(LOGTAG, "Skipping attribute " + attr);
}
}
//Don't forget this
a.recycle();
}
private void updateViews()
{
Log.d(LOGTAG, "updateViews()");
// if (getDisplayedChild() == 0) //first child - textview
if (!inEditMode) //first child - textview
{
if (ourData.isEmpty())
{
if (hintColor != null) face.setTextColor(hintColor);
face.setText(hint);
} else
{
face.setTextColor(textColor);
face.setText(ourData);
}
face.setFocusable(true);
face.setFocusableInTouchMode(true);
face.setAutoLinkMask(autoLinkMask);
} else
{ //second child - edittext
editor.setFocusable(true);
editor.setFocusableInTouchMode(true);
if (ourData.startsWith(prefillData) || ourData.length() >= prefillData.length())
editor.setText("");
else
editor.setText(prefillData);
editor.append(ourData);
inputReady = false;
editor.requestFocus();
}
}
public void setAutoLinkMask(LinkifyEnum linkifyEnumConstant)
{
switch (linkifyEnumConstant)
{
case ALL:
autoLinkMask = Linkify.ALL;
break;
case EMAIL_ADDRESSES:
autoLinkMask = Linkify.EMAIL_ADDRESSES;
break;
case MAP_ADDRESSES:
autoLinkMask = Linkify.MAP_ADDRESSES;
break;
case PHONE_NUMBERS:
autoLinkMask = Linkify.PHONE_NUMBERS;
break;
case WEB_URLS:
autoLinkMask = Linkify.WEB_URLS;
break;
case NONE:
default:
autoLinkMask = 0;
break;
}
//set it now
face.setAutoLinkMask(autoLinkMask);
}
public enum LinkifyEnum
{
ALL, EMAIL_ADDRESSES, MAP_ADDRESSES, PHONE_NUMBERS, WEB_URLS, NONE
};
}
I'm still working out some focus-related issues but this works as intended. When I use onFocuslistener 1, you can't focus from one TextBox to the other; when the textbox itself is focusable, I can focus from one to the other just fine, but I cannot inter-focus thru children and thus can't focus on the edittext to type.
the XML file:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
<TextView
android:id="#+id/TBOD_textview"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:autoLink="email"
android:focusable="true"
android:focusableInTouchMode="true"
android:linksClickable="true"
android:textAppearance="?android:attr/textAppearanceMedium" />
<RelativeLayout
android:id="#+id/TBOD_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
<EditText
android:id="#+id/TBOD_edittext"
android:layout_width="300dp"
android:layout_height="30dp"
android:layout_below="#+id/TBOD_textview"
android:focusable="true"
android:focusableInTouchMode="true"
android:imeOptions="actionDone"
android:inputType="none"
android:maxLines="1"
android:padding="2dp"
android:singleLine="true"
android:textColor="#android:color/black"
android:textSize="14dp" />
<ImageButton
android:id="#+id/TBOD_accept"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="#+id/TBOD_edittext"
android:layout_marginLeft="15dp"
android:layout_toRightOf="#+id/TBOD_edittext"
android:background="#drawable/button_accept_selector" />
<ImageButton
android:id="#+id/TBOD_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="#+id/TBOD_edittext"
android:layout_marginLeft="5dp"
android:layout_toRightOf="#+id/TBOD_accept"
android:background="#drawable/button_cancel_selector" />
<View
android:id="#+id/TBOD_top"
android:layout_width="fill_parent"
android:layout_height="0dp"
android:layout_alignParentTop="true"
android:background="#android:color/transparent" />
<View
android:id="#+id/TBOD_bottom"
android:layout_width="fill_parent"
android:layout_height="0dp"
android:layout_alignParentBottom="true"
android:background="#android:color/transparent" />
</RelativeLayout>
</RelativeLayout>
and finally, the attrs.xml file:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="TextBoxOnDemand">
<attr name="android:text" />
<attr name="android:inputType" />
<attr name="android:hint" />
<attr name="android:textColor" />
<attr name="android:textColorHint" />
<attr name="android:linksClickable" />
<attr name="android:autoLink" />
</declare-styleable>
</resources>
This is how I used it in my main xml (after including the required namespace add):
<com.shark.widget.TextBoxOnDemand
android:id="#+id/profile_email2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="#+id/profile_skypename"
android:layout_below="#+id/profile_email_placeholder"
android:hint="#string/add_email"
android:inputType="textEmailAddress"
android:textColor="#android:color/white"
android:textColorHint="#color/skype_blue" />
EDIT: I've debugged the focus issues. It turns out that giving focus to children is difficult unless you call
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
Which kinda remedies the issue but still doesn't solve it. After some while of playing around with the onFocusChange() listener still trying to get the perfect behaviour, I threw in the towel and put in added two focus guards. I realized I cannot track the loss of focus only on my container (due to it never receiving focus) but I might as well track the idea of wanting to move away from the edit field... So i went the dirty route and added two invisible bar-like views to sandwitch the edittext in between. Once they got the focus, I could hide the component and ensure they transition properly.
And there it is, now it works as it should. Thanks to all who participated.
EDIT3: final polished version, i dumped the custom tags because they simply don't work reliably enough. Lesson to be learned: if there is an android tag for something, don't bother cloning it.