Scope of the project
When a user touches the Android screen with two fingers, draw a "Frame" at each touch location with a "cursor" for each frame. Each frame is a custom slider that the cursor will move up and down. All the way up will be 100%, middle will be 0% and all the way down will be -100%. This will be used to control small motors, similar to tank turning, each touch controls a separate motor (sending signals over bluetooth). After a two touch and everything is drawn, I want to be able to lift off either finger, BUT keep the cursor at what ever location it was last at, while the other finger is free to move its cursor. When the last finger is lifted off, everything "hides" and resets to 0%.
Functionality Wanted
On two finger touch, draw separate .pngs under the touch location
After the frames and cursors are drawn, keep track of where they are relative to the frame to determine the percentage.
If a finger is lifted off, keep that fingers cursor at last known location, but the other finger can move it's cursor. Also if the finger is put back down it should be able to move its cursor again.
If both fingers are lifted off of the screen, hide everything and reset percentages to 0%
Functionality Obtained
I can draw the frames and cursors on multitouch
Positions and percentages work fine
Cursors do move properly
What doesn't work
I am unsure if I should have one custom class that handles both touch event or if i should have 2 instances of the custom class each handling their own touch events (I have tried both, the only way i get any "real" functionality is with 1 custom class handling both touch events, the other way doesn't work as intended)
When I only have 1 custom class, It works great, but I have it "hide" everything if both fingers are not on the screen, and sometimes android registers that I have lifted a finger off the screen and this causes me a lot of issues when the frames hide then re appear in a different location
When I use 2 custom classes I touch each custom class would have its own touch event, and i wouldn't have to worry about multitouch if i split the classes evenly between the screen. This was not the case, still need to deal with multitouch
Can someone explain to me how android handles their touch events. from what I have done, it seems if i lay down finger 1, the finger 2, the first finger will register a "ACTION_DOWN" and the second will register a "ACTION_POINTER_2_DOWN", BUT if i life off my first finger, my second finger is "demoted" and now all of the events my second finger registers does not related to "ACTION_POINTER_2" and instead will be "ACTION_DOWN, ACTION_UP, etc". Is this correct?
TouchUI.java
package com.robota.android;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.ImageView;
public class TouchUI extends ImageView {
public static final String LEFT_TOUCHUI = "com.robota.android:id/leftTouchUI";
public static final String RIGHT_TOUCHUI = "com.robota.android:id/rightTouchUI";
private String whoAmI = new String();
private MyPoints framePts = new MyPoints();
private MyPoints cursorPts = new MyPoints();
private Bitmap frame;
private Bitmap cursor;
private int frameWidth;
private int frameHeight;
private int cursorHeight;
private boolean pointerDown = false;
private int dy;
public TouchUI(final Context context, final AttributeSet as){
super(context, as);
Log.d("TouchUI", getResources().getResourceName(this.getId()));
whoAmI = new String(getResources().getResourceName(this.getId()));
if(whoAmI.equals(LEFT_TOUCHUI)){
frame = BitmapFactory.decodeResource(getResources(), R.drawable.tank_left);
}else if(whoAmI.equals(RIGHT_TOUCHUI)){
frame = BitmapFactory.decodeResource(getResources(), R.drawable.tank_right);
}
cursor = BitmapFactory.decodeResource(getResources(), R.drawable.cursor);
frameWidth = frame.getWidth();
frameHeight = frame.getHeight();
cursorHeight = cursor.getHeight();
}
public void determinePointers(int x, int y){
framePts.setOrigin(x-frameWidth/2, y-frameHeight/2);
cursorPts.setOrigin(x-frameWidth/2, y-frameHeight/2);
}
#Override
public boolean onTouchEvent(MotionEvent e){
int x = 0;
int y = 0;
Log.d("TouchUI", ">>>>> " + whoAmI);
if(e.getAction() == MotionEvent.ACTION_DOWN){
determinePointers(x,y);
pointerDown = true;
}else if(e.getAction() == MotionEvent.ACTION_UP){
pointerDown = false;
}else if(e.getAction() == MotionEvent.ACTION_MOVE){
dy = (int)e.getY()-framePts.getY();
if(dy <= 0){
dy=0;
}else if(dy+cursorHeight/2 >= frameHeight){
dy=frameHeight;
}
sendMotorSpeed(dy);
}
return true;
}
public void sendMotorSpeed(int dy){
float motor = dy;
motor-=frameHeight;
motor*=-1;
motor = (motor/frameHeight)*255;
PacketController.updateMotorSpeeds(whoAmI, (int)motor);
}
public void onDraw(Canvas canvas){
if(pointerDown){//twoDown){
canvas.drawBitmap(frame, framePts.getX(), framePts.getY(), null);
canvas.drawBitmap(cursor, cursorPts.getX(), (cursorPts.getY()+dy), null);
}
invalidate();
}
private class MyPoints{
private int x = -100;
private int y = -100;
private int deltaY = 0;;
public MyPoints(){
this.x = 0;
this.y = 0;
}
public int getX(){
return this.x;
}
public int getY(){
return this.y;
}
public void setOrigin(int x, int y){
this.x = x;
this.y = y;
}
public int getDeltaY(){
return deltaY;
}
public void setDeltaY(int newY){
deltaY = (newY-y);
Log.d("TouchUI", "DY: " + deltaY);
}
}
}
Main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/parentLayout"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<LinearLayout android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.robota.android.TouchUI xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/leftTouchUI"
android:background="#0000"
android:layout_height="match_parent"
android:layout_width="wrap_content"
android:layout_weight="1">
</com.robota.android.TouchUI>
<com.robota.android.TouchUI xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/rightTouchUI"
android:background="#0000"
android:layout_height="match_parent"
android:layout_width="wrap_content"
android:layout_weight="1">
</com.robota.android.TouchUI>
</LinearLayout>
RobotController.java (Main Activity Class)
package com.robota.android;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
public class RobotController extends Activity {
// Tag used to keep track of class in the Log
private static final String TAG = "robotController_new";
// Boolean to debugging
private static final boolean D = true;
// Intent request codes
private static final int DISCONNECT_DEVICE = 1;
private static final int CONNECT_DEVICE = 2;
private static final int REQUEST_ENABLE_BT = 3;
// Handler Codes
public static final int MESSAGE_READ = 1;
public static final int MESSAGE_WRITE = 2;
// Local Bluetooth Adapter
private BluetoothAdapter bluetoothAdapter = null;
// Bluetooth Discovery and Datahandler
private BluetoothComm btComm = null;
// Debug's TextView, this is where strings will be written to display
private TextView tv;
private ScrollView sv;
/** Called when the activity is first created. */
#Override
public void onCreate(Bundle savedInstanceState) {
if(D) Log.d(TAG, "++ON CREATE++");
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.main);
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if(bluetoothAdapter == null){
if(D) Log.d(TAG, "NO BLUETOOTH DEVICE");
Toast.makeText(this, "Bluetooth is not available", Toast.LENGTH_SHORT).show();
finish();
return;
}
PacketController.controller = this;
}
public void onStart(){
super.onStart();
if(D) Log.d(TAG, "++ON START++");
if(!bluetoothAdapter.isEnabled()){
Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
}else{
// Start BluetoothComm
if(btComm == null){
setupComm();
}
}
}
/**
* Creates new Bluetooth Communication
*/
private void setupComm(){
if(D) Log.d(TAG, "+++setupComm+++");
btComm = new BluetoothComm(this, handler);
}
private void connectDevice(Intent data){
if(D) Log.d(TAG, "+++connectDevice+++");
String addr = data.getExtras()
.getString(DeviceListActivity.EXTRA_DEVICE_ADDRESS);
BluetoothDevice device = bluetoothAdapter.getRemoteDevice(addr);
if(D) Log.d(TAG,"REMOTE ADDR: "+ addr);
btComm.connect(device);
}
private void disconnectDevice(){
if(D) Log.d(TAG, "---disconnectDevice---");
if(btComm.getState() == btComm.STATE_CONNECTED){
btComm.disconnect();
}
}
#Override
public boolean onCreateOptionsMenu(Menu menu)
{
//super.onCreateOptionsMenu(menu);
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu, menu);
return true;
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
Intent serverIntent = null;
switch(item.getItemId()){
case R.id.insecure_connect_scan:
// Launch the DeviceListActivity to see devices and do scan
serverIntent = new Intent(this, DeviceListActivity.class);
try{
startActivityForResult(serverIntent, CONNECT_DEVICE);
}catch(ActivityNotFoundException activityNotFound){
Log.e(TAG, "Could not start DeviceListActivity(Insecure)");
}
return true;
}
return false;
}
#Override
public void onActivityResult(int requestCode, int resultCode, Intent data){
switch(requestCode){
case CONNECT_DEVICE:
if(resultCode == Activity.RESULT_OK){
connectDevice(data);
}
break;
case DISCONNECT_DEVICE:
if(resultCode == Activity.RESULT_OK){
disconnectDevice();
}
break;
}
}
public Handler getHandler(){
return this.handler;
}
public BluetoothComm getBtComm(){
return this.btComm;
}
// The Handler that gets information back from the BluetoothChatService
private final Handler handler = new Handler() {
#Override
public void handleMessage(Message msg) {
if(D) Log.d(TAG, "check message");
switch (msg.what) {
case MESSAGE_READ:
if(D) Log.d(TAG, "trying to read message");
byte[] readBuf = (byte[]) msg.obj;
// construct a string from the valid bytes in the buffer
String readMessage = new String(readBuf, 0, msg.arg1);
if(D) Log.d(TAG, "bytes: " + readBuf + " arg1: " + msg.arg1 + " Message: " + readMessage);
tv.append(readMessage);
break;
case MESSAGE_WRITE:
if(D) Log.d(TAG, "trying to send message");
String sendMessage = new String(String.valueOf(msg.obj));
}
}
};
}
Any other classes not listed I didn't believe needed to be, but if they are needed please let me know.
Any help is much appreciated
You're going to need to save the pointerId's of each point and compare them to the new Id's given with each MotionEvent. It's slightly tricky to explain, so I'll point you to this ADB Post that explains it much better than I could. Long story short? Multitouch can be tricksy, but it's not as bad as it looks at first glance.
Related
I am working on a program in java (android) I would like it to start and continue asking questions, upon button click, until the user is prompted for input, once input is received the program should resume asking the questions. The program should pause when the button is clicked again.
I am new to OOP and self taught. I thought that a thread was going to be the most practical way to solve the issue. I cannot get the program to both loop and allow user input. When trying to use HandlerThread I loose the ability to input data in the EditText. Could someone help with getting this loop to run on start click and loop after input?
I have a functional program that works when the button is cycled:
Main layout begins with "START" button, on click the start button turns to "PAUSE" and a seperate repeat button is made visible (and functional).
The values are generated, the question is asked, and the user is prompted by popping up the soft keyboard and setting the cursor in the EditText field.
Once an answer is received and the "enter/done" keyboard button is clicked, the answer will be evaluated against the saved values. But I cannot get the program to loop, or if I get it to loop it skips input completely and continues to as questions with no time for input.
Please direct me on code cleanliness if needed, I want to learn what I am doing incorrectly.
MAIN.java
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.speech.tts.TextToSpeech;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import java.util.Locale;
public class MainActivity extends AppCompatActivity {
// Create appropriate objects for buttons, edit text, and text to speech
TextToSpeech tts;
EditText txt;
Button sbtn, rbtn;
// Array and int to store numbers
int[] num;
int added = 0;
// Boolean to check if questions is running
public boolean isRunning;
// Variables for random number range. TODO(Put into switch statement and list to select 1, 10, or 100s)
static int maxNum = 100;
static int minNum = 10;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Initializing buttons and others
txt = findViewById(R.id.ans);
sbtn = findViewById(R.id.strButton);
rbtn = findViewById(R.id.rptButton);
rbtn.setVisibility(View.INVISIBLE);
// Initialize text to speech engine
tts = new TextToSpeech(getApplicationContext(), new TextToSpeech.OnInitListener() {
#Override
public void onInit(int status) {
if (status != TextToSpeech.ERROR) {
tts.setLanguage(Locale.ENGLISH);
}
}
});
// Start button click listener
sbtn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
if (!isRunning) {
// Show repeat button, hide start and show pause
rbtn.setVisibility(View.VISIBLE);
sbtn.setText("Pause");
process();
isRunning = true;
} else {
sbtn.setText("Start");
rbtn.setVisibility(View.GONE);
isRunning = false;
}
}
});
// Repeat button click listener
rbtn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
// Repeat recently generated numbers
Utilities.speakNums(num[0], num[1], tts);
}
});
}
public void onPause() {
if (tts != null) {
tts.stop();
tts.shutdown();
}
super.onPause();
}
// Get input and compare with stored values, announce if user answer is correct or incorrect
public void submitAns() {
txt.setOnKeyListener(new View.OnKeyListener() {
public boolean onKey(View v, int keyCode, KeyEvent event) {
// If the event is a key-down event on the "enter/done" button
if ((event.getAction() == KeyEvent.ACTION_DOWN) &&
(keyCode == KeyEvent.KEYCODE_ENTER)) {
// Check to make sure the text field is not empty
if (TextUtils.isEmpty(txt.getText().toString())) {
Toast.makeText(MainActivity.this, "Enter a Number!", Toast.LENGTH_SHORT).show();
return false;
}
int intValue = Integer.parseInt(txt.getText().toString());
if (added == intValue) {
Toast.makeText(MainActivity.this, "Correct", Toast.LENGTH_SHORT).show();
tts.speak("Correct", TextToSpeech.QUEUE_ADD, null, null);
} else {
Toast.makeText(MainActivity.this, added + " is the Correct Answer!", Toast.LENGTH_SHORT).show();
tts.speak("Incorrect", TextToSpeech.QUEUE_FLUSH, null, null);
Utilities.speakAns(added, tts);
tts.speak("is the Correct answer", TextToSpeech.QUEUE_ADD, null, null);
}
txt.setText("");
return true;
}
return false;
}
});
}
public void process() {
num = Utilities.askQuestion(minNum, maxNum, tts);
added = Utilities.add(num[0], num[1]);
Utilities.focus(txt, getApplicationContext());
submitAns();
}
}
UTILITIES.java
import android.content.Context;
import android.speech.tts.TextToSpeech;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import java.util.Random;
public class Utilities {
// Function to generate random numbers in range
public static int randomGen(int minNum, int maxNum) {
final Random randNum = new Random();
return randNum.nextInt(maxNum - minNum) + minNum;
}
public static int add(int num1, int num2) {
return num1 + num2;
}
public static int sub(int num1, int num2) {
return num1 - num2;
}
// Speak individual numbers with operator in between speech
public static void speakNums(int r1, int r2, TextToSpeech tts) {
String toSpeak = Integer.toString(r1);
String nexToSpeak = Integer.toString(r2);
tts.speak(toSpeak, TextToSpeech.QUEUE_ADD, null, null);
tts.speak("Plus", TextToSpeech.QUEUE_ADD, null, null);
tts.speak(nexToSpeak, TextToSpeech.QUEUE_ADD, null, null);
}
// Speak answer
public static void speakAns(int a, TextToSpeech tts) {
String sumSpeak = Integer.toString(a);
tts.speak(sumSpeak, TextToSpeech.QUEUE_ADD, null, null);
}
// Request focus so that keyboard pops up as generate button is tapped
public static void focus(EditText txt, Context context) {
txt.requestFocus();
InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(txt, InputMethodManager.SHOW_IMPLICIT);
}
// Generate question, speak question, and return array of random numbers for other operations
public static int[] askQuestion(int minNum, int maxNum, TextToSpeech tts) {
int r1 = randomGen(minNum, maxNum);
int r2 = randomGen(minNum, maxNum);
speakNums(r1, r2, tts);
return new int[] { r1, r2};
}
}
If you want to have a repeating/infinite series of events that involve user interaction (like answering a question) you should set it up as a series of chained events rather than trying to use an actual code loop or handler thread.
An example of how this could be set up is:
Generate a question to show (e.g. "What is 12 + 54?")
When the user is done answering, call a "submit" method that checks their answer and either shows an error or generates a new question to show.
Repeat the cycle above for as long as you want. No loops or handler threads are needed for this.
In terms of architecture, separating out as much of the question-generation and answer-processing logic into a ViewModel will help you tremendously, then the activity can just observe the relevant state in the view model (like what question to show).
Here is a simple example based on the description and example code you provided. There is a ViewModel that handles creating a question, checking the answer, and advancing to a new question, and an Activity that observes the relevant state from the ViewModel. Try it out in a blank app project to understand how it works.
ViewModel
public class MainViewModel extends ViewModel {
// Error string to observe - to show an error message or toast
private final MutableLiveData<String> error = new MutableLiveData<>("");
LiveData<String> getError() {
return error;
}
// Current question to show
private final MutableLiveData<String> question = new MutableLiveData<>("");
LiveData<String> getQuestion() {
return question;
}
// Text to show on the start/pause button
private final MutableLiveData<String> startPauseButton = new MutableLiveData<>("START");
LiveData<String> getStartPauseButton() {
return startPauseButton;
}
// private internal state, e.g. current question,
// expected answer, play/pause state
private int expected = 0;
private String current_question = "";
private boolean playing = false;
private final Random random = new Random();
private final int minNum = 10;
private final int maxNum = 100;
private int getNumber() {
return random.nextInt(maxNum - minNum) + minNum;
}
// Process a user's answer, and either show an error
// message or generate a new question to show
void submitAnswer(String ans) {
try {
int a = Integer.parseInt(ans);
if( a == expected ) {
generateNewQuestion();
question.postValue(current_question);
}
else {
error.postValue("Incorrect answer, try again");
}
}
catch (NumberFormatException e) {
error.postValue("Not a number - enter a number");
}
}
private void generateNewQuestion() {
int a = getNumber();
int b = getNumber();
expected = a + b;
current_question = "What is " + a + " + " + b + "?";
}
void clearError() {
error.postValue("");
}
// Called when the user clicks the play/pause button
void clickStartPause() {
playing = !playing;
if( playing ) {
startPauseButton.postValue("PAUSE");
question.postValue(current_question);
}
else {
startPauseButton.postValue("START");
question.postValue("");
}
}
public MainViewModel() {
generateNewQuestion();
}
}
Activity
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView question = findViewById(R.id.question);
EditText answer = findViewById(R.id.answer);
Button start = findViewById(R.id.start);
Button submit = findViewById(R.id.submit);
question.setVisibility(View.GONE);
answer.setVisibility(View.GONE);
submit.setVisibility(View.GONE);
MainViewModel model = new ViewModelProvider(this).get(MainViewModel.class);
// Observe the current question, and if it is blank
// hide the question/answer/submit views
final Observer<String> questionObserver = questionTxt -> {
if( questionTxt == null || questionTxt.isEmpty() ) {
question.setVisibility(View.GONE);
answer.setVisibility(View.GONE);
submit.setVisibility(View.GONE);
}
else {
question.setVisibility(View.VISIBLE);
answer.setVisibility(View.VISIBLE);
submit.setVisibility(View.VISIBLE);
question.setText(questionTxt);
}
};
// Observe the error state, if it is non-blank show
// a toast then reset the state (so the toast only shows once)
final Observer<String> errorObserver = errorText -> {
if( errorText != null && !errorText.isEmpty() ) {
Toast.makeText(this, errorText, Toast.LENGTH_LONG).show();
model.clearError();
}
};
model.getError().observe(this, errorObserver);
model.getQuestion().observe(this, questionObserver);
model.getStartPauseButton().observe(this, start::setText);
submit.setOnClickListener(v -> {
model.submitAnswer(answer.getText().toString());
answer.setText("");
});
start.setOnClickListener(v -> model.clickStartPause());
}
XML
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="#+id/question"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<EditText
android:id="#+id/answer"
android:hint="Answer"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="#id/question"
android:layout_width="0dp"
android:layout_height="wrap_content" />
<Button
android:id="#+id/start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Start"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/answer"/>
<Button
android:id="#+id/submit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Submit"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="#id/answer"/>
</androidx.constraintlayout.widget.ConstraintLayout>
I have search now for hours through the internet and have found nothing substantial so far. The thing that I want to do is a multi choice preference view, that disables the last item and reenables it, if it is not alone anymore.
I through so far about taking the super class force read the private variables in there to write my own onPrepareDialogBuilder(AlertDialog.Builder builder). Which is configuring its own OnMultiChoiceClickListener that jumps in, in the moment where has only one item left. The problem here is, that I use a bad practice force read of a private variable and that I have so far no idea how to get the checkbox item and how to disable it. But I think looking even deeper into the Android SDK will solve this problem.
At the end, if nothing works, solving the problem with doing an overwrite the OnPreferenceChangeListener to display a toast if the user has less than one item selected. But user friendliness is a high value, that needs to be earned and that often isn't easy.
Thx.
import android.content.Context;
import android.preference.MultiSelectListPreference;
import android.util.AttributeSet;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import georg.com.flironetest_01.Variables.Units;
/**
* Created by Georg on 16/03/16.
*/
public class UnitMultipleSelectorPreference extends MultiSelectListPreference {
public UnitMultipleSelectorPreference(Context context, AttributeSet attrs) {
super(context, attrs);
List<CharSequence> humanU = new ArrayList<>();
List<CharSequence> machineU = new ArrayList<>();
Units[] all = Units.values(); // Units is a enum with a rewriten to string statement.
for (Units elem : all) {
humanU.add(elem.toString());
machineU.add(elem.name());
}
setEntries(humanU.toArray(new CharSequence[humanU.size()]));
setEntryValues(machineU.toArray(new CharSequence[machineU.size()]));
Set<String> mU = new HashSet<>();
mU.add(Units.C.name());
mU.add(Units.K.name());
setDefaultValue(mU);
}
}
Okay. To answer my own question here after the motto "self is the man": I ended up with programming my own preference panel. Below is the code. If somebody likes to look over it and give some times how to make it even more stable: feel free.
But to sum up what I did: I created my own ArrayAdapter. But DialogPreference didn't allowed me to create my own multi selector. You need to change the final dialog fragment to create a working multi selector list (see here: https://stackoverflow.com/a/17907379/5759814). That is not an easy task if you work with the DialogPreferences. The reason is these few amounts of code:
/**
* Shows the dialog associated with this Preference. This is normally initiated
* automatically on clicking on the preference. Call this method if you need to
* show the dialog on some other event.
*
* #param state Optional instance state to restore on the dialog
*/
protected void showDialog(Bundle state) {
Context context = getContext();
mWhichButtonClicked = DialogInterface.BUTTON_NEGATIVE;
mBuilder = new AlertDialog.Builder(context)
.setTitle(mDialogTitle)
.setIcon(mDialogIcon)
.setPositiveButton(mPositiveButtonText, this)
.setNegativeButton(mNegativeButtonText, this);
View contentView = onCreateDialogView();
if (contentView != null) {
onBindDialogView(contentView);
mBuilder.setView(contentView);
} else {
mBuilder.setMessage(mDialogMessage);
}
onPrepareDialogBuilder(mBuilder);
getPreferenceManager().registerOnActivityDestroyListener(this);
// Create the dialog
final Dialog dialog = mDialog = mBuilder.create();
if (state != null) {
dialog.onRestoreInstanceState(state);
}
if (needInputMethod()) {
requestInputMethod(dialog);
}
dialog.setOnDismissListener(this);
dialog.show();
}
As you can see here is a method triggered to change my dialog builder with onPrepareDialogBuilder, but it doesn't seem like that there is any other function triggered afterwards, that would allow me to change the dialog directly after its creation. And the second idea of changing the onPrepareDialogBuilder so that I can init everything there, doesn't really help, because I end up with displayed dialog windows. That lead me to my decision of creating my completely own Preference class. With that decision I loose all those nice prepared functions like onRestoreInstanceState and Co, but I now have an application with a much more persistent flow, that doesn't do any stupid things when I select zero units for my thermal view.
Below the non commented code. I'm sorry, but I think its simple enough for everybody who landing here.
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.preference.Preference;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import georg.com.flironetest_01.Variables.Units;
/**
* Created by Georg on 16/03/16.
*/
public class UnitMultipleSelectorPreference extends Preference implements DialogInterface.OnClickListener, Preference.OnPreferenceClickListener {
String[] human_entries = null;
String[] machine_entries = null;
public SharedPreferences prev;
public UnitMultipleSelectorPreference(Context context, AttributeSet attrs) {
super(context, attrs);
prev = getSharedPreferences();
List<String> humanU = new ArrayList<>();
List<String> machineU = new ArrayList<>();
Units[] all = Units.values();
for (Units elem : all) {
humanU.add(elem.toString());
machineU.add(elem.name());
}
human_entries = humanU.toArray(new String[humanU.size()]);
machine_entries = machineU.toArray(new String[machineU.size()]);
Set<String> mU = new HashSet<>();
mU.add(Units.C.name());
mU.add(Units.K.name());
setDefaultValue(mU);
setOnPreferenceClickListener(this);
}
boolean[] selected = new boolean[0];
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
if (prev == null)
return;
if (human_entries == null || machine_entries == null || human_entries.length != machine_entries.length ) {
throw new IllegalStateException(
"ListPreference requires an entries array and an entryValues array which are both the same length");
}
selected = new boolean[human_entries.length];
for (int i = 0; i < human_entries.length; i++)
selected[i] = prefSet.contains(machine_entries[i]);
String[] stringObj = new String[human_entries.length];
int i = 0;
for(CharSequence ch : human_entries)
stringObj[i++] = ch.toString();
builder.setAdapter(new MyAdapter(getContext(), android.R.layout.simple_list_item_multiple_choice, stringObj), new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int which) {
}
});
AlertDialog mDialog = builder.create();
mDialog.getListView().setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE);
mDialog.getListView().setItemsCanFocus(false);
mDialog.getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
// Manage selected items here
ListView mParent = (ListView)parent;
if (mParent.getCheckedItemCount() >= 1)
selected[position] = mParent.isItemChecked(position);
if (mParent.getCheckedItemCount() == 0)
mParent.setItemChecked(position, true);
}
});
mDialog.show();
i = 0;
for (boolean select : selected)
mDialog.getListView().setItemChecked(i++, select);
}
#Override
public boolean onPreferenceClick(Preference preference) {
AlertDialog.Builder mBuilder = new AlertDialog.Builder(getContext());
mBuilder.setTitle(getTitle())
.setPositiveButton(android.R.string.ok, this)
.setNegativeButton(android.R.string.cancel, this);
onPrepareDialogBuilder(mBuilder);
return true;
}
#Override
public void onClick(DialogInterface dialog, int which) {
Toast.makeText(getContext(), "W:"+which + " | " + Arrays.toString(selected), Toast.LENGTH_SHORT).show();
switch (which) {
case -1:
if (isPersistent()) {
prefSet = new HashSet<>();
for (int i = 0; i < selected.length; i++) {
if (selected[i])
prefSet.add(machine_entries[i]);
}
getEditor().putStringSet(getKey(), prefSet).apply();
Toast.makeText(getContext(), "W:"+which + " | " + getSharedPreferences().getStringSet(getKey(),null).toString(), Toast.LENGTH_SHORT).show();
}
return;
}
}
public class MyAdapter extends ArrayAdapter<String> {
public MyAdapter(Context context, int textViewResourceId, String[] objects) {
super(context, textViewResourceId, objects);
}
#Override
public boolean isEnabled(int n) {
return true;
}
}
Set<String> prefSet;
#Override
protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) {
super.onSetInitialValue(restorePersistedValue, defaultValue);
prev = getSharedPreferences();
if(restorePersistedValue) {
prefSet = prev.getStringSet(getKey(), new HashSet<String>());
} else {
try {
prefSet = (Set<String>)defaultValue;
if(isPersistent())
getEditor().putStringSet(getKey(), prefSet);
} catch (ClassCastException e) {
Log.e("ERROR_CAST", "Error casting the default value to Set<String>.");
}
}
}
}
A really simple solution is to set a setOnPreferenceChangeListener and just return false if the new value would be empty.
All of the code is put into onCreatePreferences.
MultiSelectListPreference infoPreference = findPreference("information");
infoPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
#Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
if (size(newValue) == 0){
return false;
}
return true;
}
});
package com.game.crazyeights;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.view.MotionEvent;
import android.view.View;
import com.game.crazyeights.R;
public class TitleView extends View {
private Bitmap titleGraphic;
private Bitmap playButtonUp;
private Bitmap playButtonDown;
private boolean playButtonPressed;
private int screenH;
private int screenW;
public TitleView(Context context) {
super(context);
titleGraphic = BitmapFactory.decodeResource(getResources(), R.drawable.titlegraphic);
playButtonUp = BitmapFactory.decodeResource(getResources(), R.drawable.play_button_up);
playButtonDown = BitmapFactory.decodeResource(getResources(), R.drawable.play_button_down);
}
#Override
public void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
screenW = w;
screenH = h;
}
#Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(titleGraphic, (screenW-titleGraphic.getWidth())/2 , 0, null);
if (playButtonPressed) {
canvas.drawBitmap(playButtonDown, (screenW-playButtonUp.getWidth())/2, (int)(screenH*0.7), null);
} else {
canvas.drawBitmap(playButtonUp, (screenW-playButtonUp.getWidth())/2, (int)(screenH*0.7), null);
}
}
public boolean onTouchedEvent(MotionEvent event) {
int eventaction = event.getAction();
int X = (int)event.getX();
int Y = (int)event.getY();
switch (eventaction) {
case MotionEvent.ACTION_DOWN:
if (X > (screenW-playButtonUp.getWidth())/2 && X < (screenW-playButtonUp.getWidth())/2 + playButtonUp.getWidth() && Y > (int) (screenH*0.7) && Y < (int) (screenH*0.7) + playButtonUp.getHeight()) {
playButtonPressed = true;
}
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
playButtonPressed = false;
break;
}
invalidate();
return true;
}
}
Basically, I tried to put a invalidate at the end to redraw the screen to update the buttons state. What I mean by that is when the button is pressed down on it has a picture of it in a different state I wanna update to but, it doesn't do that when I use the invalidate. And when the user lets go of the button it updates back to the regular button state image..
If you didn't understand what I said above ill try to simplify it. I want the button to update its image when pressed down on, and to go back to its previous image when you stop pressing the button. I tried using invalidate but, it didn't work. Any idea here?
You should not place invalidate() at the end of the method. OnTouchedEvent() is fired every time you touch or move within that View.
Make sure that case within MotionEvent.ACTION_DOWN is called. I mean those boolean expressions are valid. Then call invalidate() at the end of that case.
Also I this this method is cumbersome and unnecessary. Instead you can use ImageView.
Good luck!
I have managed, with great difficulty, to make a bitmap overlay the screen. I can also get touch input, however it gets touch input for EVERYWHERE on the screen.
I want to know how I would be able to check if the touch was on my bitmap, which is visible on the screen.
The service and view class is below. I have thought and thought, but I couldn't think of a way to do it :(
package <package>;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Toast;
public class MyService extends Service {
ButtonView mView;
Bitmap bit;
#Override
public IBinder onBind(Intent intent) {
return null;
}
#Override
public void onCreate() {
super.onCreate();
bit = BitmapFactory.decodeResource(getResources(), R.drawable.button);
NotificationCompat.Builder builder = new NotificationCompat.Builder(
this);
builder.setContentTitle("Ingress Tools Running");
builder.setContentText("Click to stop Ingress Tools");
builder.setSmallIcon(R.drawable.ic_launcher);
builder.setContentIntent(PendingIntent.getActivity(this, 0, new Intent(
this, StopActivity.class), 0));
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
manager.notify(1, builder.build());
mView = new ButtonView(this, bit);
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.FILL_PARENT,
WindowManager.LayoutParams.FILL_PARENT,
WindowManager.LayoutParams.TYPE_PHONE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
PixelFormat.TRANSLUCENT);
params.gravity = Gravity.RIGHT;
params.setTitle("Load Average");
WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
wm.addView(mView, params);
}
#Override
public void onDestroy() {
super.onDestroy();
Toast.makeText(getBaseContext(), "onDestroy", Toast.LENGTH_LONG).show();
if (mView != null) {
((WindowManager) getSystemService(WINDOW_SERVICE))
.removeView(mView);
mView = null;
}
}
}
class ButtonView extends ViewGroup {
private Paint mLoadPaint;
private Rect r;
private Bitmap bit;
public ButtonView(Context context, Bitmap bit) {
super(context);
Toast.makeText(context, "HUDView", Toast.LENGTH_LONG).show();
mLoadPaint = new Paint();
mLoadPaint.setAntiAlias(true);
mLoadPaint.setTextSize(10);
mLoadPaint.setARGB(255, 255, 0, 0);
r = new Rect();
r.set(380, 134, 468, 213);
this.bit = bit;
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//canvas.drawColor(Color.BLACK);
canvas.drawBitmap(bit, 100, 100, null);
}
#Override
protected void onLayout(boolean arg0, int arg1, int arg2, int arg3, int arg4) {
}
#Override
public boolean onTouchEvent(MotionEvent event) {
int area = bit.getWidth() * bit.getHeight();
//if (event.getY() <= maxY && event.getX() <= maxX) {
Toast.makeText(getContext(), "Open tools: ", Toast.LENGTH_LONG)
.show();
//}
return true;
}
}
this works for any view
#Override
public boolean dispatchTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
if (inViewInBounds(myButtonView, (int) event.getRawX(), (int) event.getRawY())) {
// User moved outside bounds
Log.e("dispatchTouchEvent", "you touched inside button");
} else {
Log.e("dispatchTouchEvent", "you touched outside button");
}
}
return super.dispatchTouchEvent(event);
}
Rect outRect = new Rect();
int[] location = new int[2];
private boolean inViewInBounds(View view, int x, int y) {
view.getDrawingRect(outRect);
view.getLocationOnScreen(location);
outRect.offset(location[0], location[1]);
return outRect.contains(x, y);
}
Consider using FrameLayout (or any other subclass of ViewGroup) instead of ViewGroup directly. Because your current implementation of onLayout method is not correct, which will lead you to problems with displaying of child views.
Now, closer to your question. You should ininitialize Rect and just store left, top, right and bottom position of your Bitmap. As I can see, currently you're initialized r variable, but not using it anywhere.
So, you can initialize it like this:
r = new Rect(100, 100, 100 + bit.getWidth(), 100 + bit.getHeight());
Now in onTouchEvent you can just check:
r.contains((int) event.getX(), (int) event.getY());
Rect rect = new Rect();
getHitRect(rect);
if (rect.contains((int) event.getX(), (int) event.getY())) {}
you can use getHitRect(Rect). it returns the Hit rectangle in parent's coordinates. Here the documentation
Use if statement with method if(r.contains(x, y)) on that button which you want to check. This method will return true, when x and y point is inside rectangle r. You can also make public method within that class, so you can access it outside ButtonView class with button object reference.
// We assume MotionEvent is from the direct View parent so we are in the same co-ordindate space
fun View.isWithinBounds(event: MotionEvent): Boolean {
val rect = Rect(x.roundToInt(), y.roundToInt(), (x + width).roundToInt(), (y + height).roundToInt())
return rect.contains(event.x.roundToInt(), event.y.roundToInt())
}
When the "touch event" happens, it's going through all view's tree.
F.e. if you have Linear layout and ImageView on it and user touchs the screen on ImageView, then touch event intercepts and firstly it'll be handled at LinearLayour and then at the ImageView.
If you want to block event f.e. on the bitmap, then you should override onTouchEvent for Bitmap and return true value. This will mean that you handled this event and it won't be available for LinearLayout.
image.setOnTouchListener( new View.OnTouchListener() {
#Override
public boolean onTouch(View view, MotionEvent motionEvent) {
return true; // attentively read documentation for onTouc interface
}
});
I am trying with no success to modify the code example from:
http://www.inter-fuser.com/2009/08/android-animations-3d-flip.html
so it will rotate the images in a loop, when clicking on the image once. (second click should pause).
I tried using Handler and threading but cannot update the view since only the main thread can update UI.
Exception I get from the code below:
android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
[in 'image1.startAnimation(rotation);' ('applyRotation(0, 90);' from the main thread)]
package com.example.flip3d;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.widget.ImageView;
public class Flip3d extends Activity {
private ImageView image1;
private ImageView image2;
private boolean isFirstImage = true;
/** Called when the activity is first created. */
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
image1 = (ImageView) findViewById(R.id.image01);
image2 = (ImageView) findViewById(R.id.image02);
image2.setVisibility(View.GONE);
image1.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
if (isFirstImage) {
applyRotation(0, 90);
isFirstImage = !isFirstImage;
} else {
applyRotation(0, -90);
isFirstImage = !isFirstImage;
}
}
});
}
private void applyRotation(float start, float end) {
// Find the center of image
final float centerX = image1.getWidth() / 2.0f;
final float centerY = image1.getHeight() / 2.0f;
// Create a new 3D rotation with the supplied parameter
// The animation listener is used to trigger the next animation
final Flip3dAnimation rotation =
new Flip3dAnimation(start, end, centerX, centerY);
rotation.setDuration(500);
rotation.setFillAfter(true);
rotation.setInterpolator(new AccelerateInterpolator());
rotation.setAnimationListener(new DisplayNextView(isFirstImage, image1, image2));
if (isFirstImage)
{
image1.startAnimation(rotation);
} else {
image2.startAnimation(rotation);
}
}
}
How can I manage to update the UI and control the rotation within onClick listener?
Thank you,
Oakist
Try wrapping the UI code in runOnUIThread()