Showing posts with label android. Show all posts
Showing posts with label android. Show all posts
Friday, May 1, 2015
Donors, thank you!
In the past two months I've received several in-app purchase donations for my Napkin paint program. Thank you, kind people!
Thursday, July 3, 2014
Finally published Napkin Ideas on Play.
Well, finally.
This was actually done months ago, but I thought it would be really nice to have an ability to edit the background. So I started creating a wizard to do that, and for whatever reason, I just could not get it working the way I wanted, and got kind of depressed about that. So I dropped that bit out of the code and decided to publish as it was before.
Well, almost as it was before, with one notable exception: I took out the advertising. It's annoying to look at, and probably not going to generate any significant amount of revenue. In its place, I have put in what I'm calling a beg bar: give me a donation, the little yellow bar goes away.
This was actually done months ago, but I thought it would be really nice to have an ability to edit the background. So I started creating a wizard to do that, and for whatever reason, I just could not get it working the way I wanted, and got kind of depressed about that. So I dropped that bit out of the code and decided to publish as it was before.
Well, almost as it was before, with one notable exception: I took out the advertising. It's annoying to look at, and probably not going to generate any significant amount of revenue. In its place, I have put in what I'm calling a beg bar: give me a donation, the little yellow bar goes away.
Tuesday, January 14, 2014
Done with the paint app
There was a trip to Germany for the boyfriend to attend a conference, and Christmas, and New Year's, so not a whole lot got done between the 15th and the 2nd. But since then it's been chin-rubbing and poking and growling, and finally the app was finished. I still need code review but right now it does just about everything I want it to.
The sticky bit was adding text. I'd originally set up the button to open an alert dialog where you could enter your text, and select a font size via a slider. That was visually jarring and functionally awkward because you couldn't really tell how large to make your text in relation to the drawing. You would only know after placing it, so you'd have to undo and try again if you found it the wrong size.
So I found a bit of code that would include an actionable graphic (an image of an X to clear the text) in an EditText. From that starting point I made the EditText so it would be placed directly on the drawing surface, and it could be resized by dragging at the corner graphic. Clicking the checkmark graphic at the top cements the text in place. It's much smoother than having a dialog box.
After that, some code to save the drawing in JSON so it can be retrieved later.
For the next trick I want to put advertising in it. I'd like have a free-with-ads version, and a paid no-ads version. Don't hurt me, please. I'm not crazy about doing it either.
So to distract you from that last paragraph, here's the code for an edittext that can be moved around and the font resized by dragging. Both the drawing and the EditText are children of a Viewgroup.
public class MovableEditText extends EditText {
// 2 drawables: an OK and a resize handle
private Drawable closeImg = getResources().getDrawable(R.drawable.text_ok);
private Drawable resizeImg = getResources().getDrawable(R.drawable.text_resize);
private long timeFingerDown;
public InputMethodManager imm;
private App app;
private boolean resizing;
private TextCompletedListener textListener;
private boolean finished = false;
private float startDeltaLeft;
private float startDeltaTop;
float leftMargin = 100;
float topMargin = 100;
public MovableEditText(Context context) {
super(context);
app = (App) context.getApplicationContext();
imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
init();
}
@Override
public void scrollTo(int x, int y) {
super.scrollTo(0, 0);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
timeFingerDown = event.getEventTime();
// Remembers where you grabbed the text
startDeltaLeft = event.getX();
startDeltaTop = event.getY();
setCursorVisible(true);
return true;
case MotionEvent.ACTION_UP:
resizing = false;
// clicking on the OK checkbox
if (event.getY() < getPaddingTop() + closeImg.getIntrinsicHeight()) {
MovableEditText.this.okButtonClick();
}
// otherwise, act like a proper edittext
if (event.getEventTime() - timeFingerDown < 200 && !finished) {
super.onTouchEvent(event);
}
return false;
case MotionEvent.ACTION_MOVE:
if (event.getX() > getWidth() - getPaddingRight() - resizeImg.getIntrinsicWidth()) {
resizeBox(event);
// we touched the arrow, so we are resizing the box (i.e. increasing font size)
resizing = true;
return true;
}
if (event.getEventTime() - timeFingerDown > 200 && !resizing) {
// we are moving the box; close the keyboard
imm.hideSoftInputFromWindow(getWindowToken(), 0);
moveBox(event);
}
return true;
case MotionEvent.ACTION_CANCEL:
resizing = false;
return false;
}
return true;
}
public void setTextCompletedListener(TextCompletedListener l) {
textListener = l;
}
private void init() {
setMinimumHeight(closeImg.getIntrinsicHeight()*2);
setMinimumWidth(closeImg.getIntrinsicWidth()+resizeImg.getIntrinsicWidth() *2);
// Set bounds of the Clear button so it will look ok
closeImg.setBounds(0, 0, closeImg.getIntrinsicWidth(), closeImg.getIntrinsicHeight());
resizeImg.setBounds(0, 0, resizeImg.getIntrinsicWidth(), resizeImg.getIntrinsicHeight());
// There may be initial text in the field, so we may need to display the button
showOkButton();
//if text changes, take care of the button
this.addTextChangedListener(new TextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
MovableEditText.this.showOkButton();
}
@Override
public void afterTextChanged(Editable arg0) {
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
});
}
private void moveBox(MotionEvent event) {
leftMargin += (int) (event.getX() - startDeltaLeft);
topMargin += (int) (event.getY() - startDeltaTop);
requestLayout();
}
// the following is to be used by drawview when resizing and panning
public void externalMove(float dragX, float dragY) {
leftMargin = (int) (leftMargin + dragX);
topMargin = (int) (topMargin + dragY);
requestLayout();
}
private void resizeBox(MotionEvent event) {
setWidth((int) event.getX());
setHeight((int) event.getY());
resizeText();
}
// change the text size to whatever will fit in the box
private void resizeText() {
String[] multiline = this.getText().toString().split("\n");
float adjustedHeight = (getMeasuredHeight()/multiline.length - getCompoundPaddingBottom() - getCompoundPaddingTop())/2;
setTextSize(adjustedHeight);
}
// if there is text, add the OK button
void showOkButton() {
if (this.getText().toString().equals("")) {
// remove the clear button
this.setCompoundDrawables(this.getCompoundDrawables()[0],null, resizeImg, this.getCompoundDrawables()[3] );
} else {
//add clear button
this.setCompoundDrawables(this.getCompoundDrawables()[0], closeImg, resizeImg, this.getCompoundDrawables()[3]);
}
}
void okButtonClick() {
MyViewGroup viewGroup = (MyViewGroup) getParent();
// get the text, get the size, get the position, create a Stroke from it
// locate this view in the viewgroup's coordinate system
// the screen coordinates minus the location of the MET
Float deltaL = getLeft() - getScrollX() - viewGroup.dv.getCenterX();
Float deltaB = getTop() - getScrollY() + getBaseline() - viewGroup.dv.getCenterY() ;
// viewcenterxy are the canvas coordinates
Float dvViewCenterX = viewGroup.dv.getViewCenterX();
Float dvViewCenterY = viewGroup.dv.getViewCenterY();
Float scaleFactor = viewGroup.dv.getScaleFactor();
// translate that delta to the canvas's coordinates
Float translatedL = dvViewCenterX + (deltaL/scaleFactor);
Float translatedB = dvViewCenterY + (deltaB/scaleFactor);
if (this.getText().toString().length()>0) {
app.setText(getText().toString());
app.setTextSize(getTextSize()/2); // i do not know why it keeps doubling the size
app.strokeInProgress = new TextStrokeBuilder(app, translatedL, translatedB, scaleFactor);
app.finishStroke();
}
// tell viewgroup to delete the EditText
textListener.onTextCompleted();
finished = true;
imm.hideSoftInputFromWindow(getWindowToken(),0);
}
public void setLeftMargin(float left) {
leftMargin = left;
}
public void setTopMargin (float top) {
topMargin = top;
}
}
The sticky bit was adding text. I'd originally set up the button to open an alert dialog where you could enter your text, and select a font size via a slider. That was visually jarring and functionally awkward because you couldn't really tell how large to make your text in relation to the drawing. You would only know after placing it, so you'd have to undo and try again if you found it the wrong size.
So I found a bit of code that would include an actionable graphic (an image of an X to clear the text) in an EditText. From that starting point I made the EditText so it would be placed directly on the drawing surface, and it could be resized by dragging at the corner graphic. Clicking the checkmark graphic at the top cements the text in place. It's much smoother than having a dialog box.
After that, some code to save the drawing in JSON so it can be retrieved later.
For the next trick I want to put advertising in it. I'd like have a free-with-ads version, and a paid no-ads version. Don't hurt me, please. I'm not crazy about doing it either.
So to distract you from that last paragraph, here's the code for an edittext that can be moved around and the font resized by dragging. Both the drawing and the EditText are children of a Viewgroup.
public class MovableEditText extends EditText {
// 2 drawables: an OK and a resize handle
private Drawable closeImg = getResources().getDrawable(R.drawable.text_ok);
private Drawable resizeImg = getResources().getDrawable(R.drawable.text_resize);
private long timeFingerDown;
public InputMethodManager imm;
private App app;
private boolean resizing;
private TextCompletedListener textListener;
private boolean finished = false;
private float startDeltaLeft;
private float startDeltaTop;
float leftMargin = 100;
float topMargin = 100;
public MovableEditText(Context context) {
super(context);
app = (App) context.getApplicationContext();
imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
init();
}
@Override
public void scrollTo(int x, int y) {
super.scrollTo(0, 0);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
timeFingerDown = event.getEventTime();
// Remembers where you grabbed the text
startDeltaLeft = event.getX();
startDeltaTop = event.getY();
setCursorVisible(true);
return true;
case MotionEvent.ACTION_UP:
resizing = false;
// clicking on the OK checkbox
if (event.getY() < getPaddingTop() + closeImg.getIntrinsicHeight()) {
MovableEditText.this.okButtonClick();
}
// otherwise, act like a proper edittext
if (event.getEventTime() - timeFingerDown < 200 && !finished) {
super.onTouchEvent(event);
}
return false;
case MotionEvent.ACTION_MOVE:
if (event.getX() > getWidth() - getPaddingRight() - resizeImg.getIntrinsicWidth()) {
resizeBox(event);
// we touched the arrow, so we are resizing the box (i.e. increasing font size)
resizing = true;
return true;
}
if (event.getEventTime() - timeFingerDown > 200 && !resizing) {
// we are moving the box; close the keyboard
imm.hideSoftInputFromWindow(getWindowToken(), 0);
moveBox(event);
}
return true;
case MotionEvent.ACTION_CANCEL:
resizing = false;
return false;
}
return true;
}
public void setTextCompletedListener(TextCompletedListener l) {
textListener = l;
}
private void init() {
setMinimumHeight(closeImg.getIntrinsicHeight()*2);
setMinimumWidth(closeImg.getIntrinsicWidth()+resizeImg.getIntrinsicWidth() *2);
// Set bounds of the Clear button so it will look ok
closeImg.setBounds(0, 0, closeImg.getIntrinsicWidth(), closeImg.getIntrinsicHeight());
resizeImg.setBounds(0, 0, resizeImg.getIntrinsicWidth(), resizeImg.getIntrinsicHeight());
// There may be initial text in the field, so we may need to display the button
showOkButton();
//if text changes, take care of the button
this.addTextChangedListener(new TextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
MovableEditText.this.showOkButton();
}
@Override
public void afterTextChanged(Editable arg0) {
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
});
}
private void moveBox(MotionEvent event) {
leftMargin += (int) (event.getX() - startDeltaLeft);
topMargin += (int) (event.getY() - startDeltaTop);
requestLayout();
}
// the following is to be used by drawview when resizing and panning
public void externalMove(float dragX, float dragY) {
leftMargin = (int) (leftMargin + dragX);
topMargin = (int) (topMargin + dragY);
requestLayout();
}
private void resizeBox(MotionEvent event) {
setWidth((int) event.getX());
setHeight((int) event.getY());
resizeText();
}
// change the text size to whatever will fit in the box
private void resizeText() {
String[] multiline = this.getText().toString().split("\n");
float adjustedHeight = (getMeasuredHeight()/multiline.length - getCompoundPaddingBottom() - getCompoundPaddingTop())/2;
setTextSize(adjustedHeight);
}
// if there is text, add the OK button
void showOkButton() {
if (this.getText().toString().equals("")) {
// remove the clear button
this.setCompoundDrawables(this.getCompoundDrawables()[0],null, resizeImg, this.getCompoundDrawables()[3] );
} else {
//add clear button
this.setCompoundDrawables(this.getCompoundDrawables()[0], closeImg, resizeImg, this.getCompoundDrawables()[3]);
}
}
void okButtonClick() {
MyViewGroup viewGroup = (MyViewGroup) getParent();
// get the text, get the size, get the position, create a Stroke from it
// locate this view in the viewgroup's coordinate system
// the screen coordinates minus the location of the MET
Float deltaL = getLeft() - getScrollX() - viewGroup.dv.getCenterX();
Float deltaB = getTop() - getScrollY() + getBaseline() - viewGroup.dv.getCenterY() ;
// viewcenterxy are the canvas coordinates
Float dvViewCenterX = viewGroup.dv.getViewCenterX();
Float dvViewCenterY = viewGroup.dv.getViewCenterY();
Float scaleFactor = viewGroup.dv.getScaleFactor();
// translate that delta to the canvas's coordinates
Float translatedL = dvViewCenterX + (deltaL/scaleFactor);
Float translatedB = dvViewCenterY + (deltaB/scaleFactor);
if (this.getText().toString().length()>0) {
app.setText(getText().toString());
app.setTextSize(getTextSize()/2); // i do not know why it keeps doubling the size
app.strokeInProgress = new TextStrokeBuilder(app, translatedL, translatedB, scaleFactor);
app.finishStroke();
}
// tell viewgroup to delete the EditText
textListener.onTextCompleted();
finished = true;
imm.hideSoftInputFromWindow(getWindowToken(),0);
}
public void setLeftMargin(float left) {
leftMargin = left;
}
public void setTopMargin (float top) {
topMargin = top;
}
}
Thursday, December 12, 2013
getting close to done
I finally managed to build a resizable EditText box. So you click on the Add Text button, and a little grey box pops up onscreen on top of your drawing. It's semi-transparent so you can see what's going on below. The keyboard pops up and you type your stuff. A double-headed arrow icon on the right: left/right will increase the width of the box, up/down will change the size of the font. Touch the checkmark icon at the top and the text will be placed. Press & hold on the text itself will let you drag it around to where you want it.
This is getting nice. It's reasonably smooth and intuitive.
There are several things I need to fix to get it fully working right. At the moment it doesn't place the text where I want, and the size is being measured in actual pixels, so if you're zoomed out on the page, it's freaking teeny tiny. These are scaling issues. The other bit is gracefully deleting the EditText when finished -- I need a hook or a listener or something that the EditText can use to tell the app it's done.
This is getting nice. It's reasonably smooth and intuitive.
There are several things I need to fix to get it fully working right. At the moment it doesn't place the text where I want, and the size is being measured in actual pixels, so if you're zoomed out on the page, it's freaking teeny tiny. These are scaling issues. The other bit is gracefully deleting the EditText when finished -- I need a hook or a listener or something that the EditText can use to tell the app it's done.
Sunday, December 1, 2013
More Play Store piracy
I wrote about this back in April. I do not browse the Play Store all that often -- once a month, maybe a bit more -- and I found another one a couple days ago. The game is the property of Mateusz Skutnik, an artist and Flash programmer lauded by escape gamers for his atmospheric, brain-twisting Submachine series.
I notified Mateusz and he confirmed that the developer did not have permission to port his work to Android. He'll be taking action on it.
Because the games they steal are by different developers, pirates' offerings are going to be very diverse in visual style. A developer making his or her own apps will have a certain style, probably more clearly seen in games than other apps. Widgets such as control buttons and menu styles will be re-used, and the flow from one activity to the next will likely have the same feel.
The ad load of a pirated game will be high. That's one thing that you can count on, because the game is free and this is the sole means of revenue.
It would be interesting if Google, with its advanced abilities to search images for faces and general concepts, applied this to the Play Store.
Sunday, November 17, 2013
working on a new app
A few days before Halloween Brian settled on his "costume": a bow tie.
Not just any ol' bowtie, but
It would be changeable at a touch. It would set the background to the shirt behind it. It would take photos. It would have a +1 button.
In about 6 hours we cobbled together a bowtie image that would cycle through the Google colors and wiggle when clicked, had a fake +1 button and a counter on the left, a subtle (Google design always runs subtle) "BETA" on the right. "Adjusting" the tie, i.e. rotating the phone back and forth, would take a photo.
There were a bunch of little quirky things to code for. Because of the front camera's placement, the entire app had to be displayed upside-down from the normal landscape mode, otherwise the camera would have been obscured by his collar. Photos taken by the camera resulted as upside-down, but flipping is easy. Since he's tallish, the camera tended to take photos of the top edge of the opposite wall, and I experimented with carving wedges from packing foam until we decided it was more trouble than it was worth. Brian was dissatisfied with the shake code I'd found and fine-tuned it to use the gyroscope and a specific sequence of motions to listen for.
And of course we didn't actually use the back camera to divine the clothing behind it. Brian picked a shirt, I slapped it on the scanner, and turned it into the background image.
But that isn't the 'new app' of the post title. I've started a paint program. Nothing that isn't the same as quite a few paint apps out there, and in fact it may be simpler than most. But it's partly for the experience and partly because I have an idea that will make it special.
The basics, of course, include:
- a palette of 10 colors, which can be customized
- fill color
- outline thickness
- brush
- point-to-point lines
- rectangles and ovals
- text (nothing fancy, just plain ol' text using the native Android font in the outline color)
- zooming, panning
- undo
- eraser (you won't believe how hard that is to figure out, partially due to sketchy Android documentation).
All colors include transparency levels.
You can take a photo or select one from your gallery to slide in as the background layer.
You can save the drawing+background to the gallery as a jpg, and share it to email, Drive, etc.
All customizable items (colors, line thickness) are saved in SharedPreferences, so you can always have that perfect 20% transparent purple you love.
I've just finished coding all the above. Neither Brian nor I are fully satisfied with the sequence of events used to add text. It's not easy to make a not-too-disruptive popup with a space for your text and a slider for your font size when there's a keyboard that will slide in and take up half your screen space.
More to come...
Not just any ol' bowtie, but
>< > the Bowtie of the Future < ><
It would be changeable at a touch. It would set the background to the shirt behind it. It would take photos. It would have a +1 button.
In about 6 hours we cobbled together a bowtie image that would cycle through the Google colors and wiggle when clicked, had a fake +1 button and a counter on the left, a subtle (Google design always runs subtle) "BETA" on the right. "Adjusting" the tie, i.e. rotating the phone back and forth, would take a photo.
There were a bunch of little quirky things to code for. Because of the front camera's placement, the entire app had to be displayed upside-down from the normal landscape mode, otherwise the camera would have been obscured by his collar. Photos taken by the camera resulted as upside-down, but flipping is easy. Since he's tallish, the camera tended to take photos of the top edge of the opposite wall, and I experimented with carving wedges from packing foam until we decided it was more trouble than it was worth. Brian was dissatisfied with the shake code I'd found and fine-tuned it to use the gyroscope and a specific sequence of motions to listen for.
And of course we didn't actually use the back camera to divine the clothing behind it. Brian picked a shirt, I slapped it on the scanner, and turned it into the background image.
But that isn't the 'new app' of the post title. I've started a paint program. Nothing that isn't the same as quite a few paint apps out there, and in fact it may be simpler than most. But it's partly for the experience and partly because I have an idea that will make it special.
The basics, of course, include:
- a palette of 10 colors, which can be customized
- fill color
- outline thickness
- brush
- point-to-point lines
- rectangles and ovals
- text (nothing fancy, just plain ol' text using the native Android font in the outline color)
- zooming, panning
- undo
- eraser (you won't believe how hard that is to figure out, partially due to sketchy Android documentation).
All colors include transparency levels.
You can take a photo or select one from your gallery to slide in as the background layer.
You can save the drawing+background to the gallery as a jpg, and share it to email, Drive, etc.
All customizable items (colors, line thickness) are saved in SharedPreferences, so you can always have that perfect 20% transparent purple you love.
I've just finished coding all the above. Neither Brian nor I are fully satisfied with the sequence of events used to add text. It's not easy to make a not-too-disruptive popup with a space for your text and a slider for your font size when there's a keyboard that will slide in and take up half your screen space.
More to come...
Thursday, May 9, 2013
reading/writing Google Drive
Here are the broad steps to write a file from an Android device to Google Drive. You create a unique SHA1 fingerprint for your application (step 1), tell Drive your application exists and will be utilising the Drive API (step 2). The branding information is the "pretty" name for your app; the package name is its name in your code (e.g. com.example.myapp). Library and dependency installation (step 3) is easy if you have Eclipse; if you have IntelliJ, one of their programmers has shown how to create the analogous configuration on StackOverflow.
The sample code, Drive Quickstart, starts up the built-in camera app, grabs the Uri of the saved image, and uploads it to Drive. In the process it needs to get a login name and authorization to write to Drive. Since life is uncertain, it uses startActivityForResult, which returns a result code to determine the next action. I needed significantly more complicated behavior for writing the file, but this is the basic sequence of events.
My application stores the user login (e.g. "mightyjoeyoung@gmail.com") in SharedPreferences, so once it's set, it won't ask again. There are some initial checks if a) a stored name exists and b) the stored name actually associates with a valid account. If it finds nothing, it starts an account picker Intent.
app = (App) getApplication();
// check for a previously saved login
settings = getSharedPreferences(app.PREFS_NAME, 0);
String savedLogin = settings.getString("savedlogin", "");
if (!savedLogin.equals("")) {
app.credential.setSelectedAccountName(savedLogin);
service = getDriveService(app.credential);
}
if (savedInstanceState != null) {
inProgress = savedInstanceState.getBoolean("inProgress");
}
if (app.credential.getSelectedAccount() == null) {
startActivityForResult(app.credential.newChooseAccountIntent(), REQUEST_ACCOUNT_PICKER);
}
When a valid account is set, it authorizes and creates a Drive service object
Drive service = new Drive.Builder(AndroidHttp.newCompatibleTransport(), new GsonFactory(), credential).build();
which is used for the reading/writing transactions. Failure to authorize results in going back to the account picker.
If all is well, it grabs the Uri of the file on the phone (which was created before starting the whole process) and attempts to upload it.
Drive Quickstart's sample code has no UI beyond the bare minimum. While it's busy trying to upload, by all appearances the app has hung. And the first time it's run, it can take up to a minute to complete the process (or at least that's what happened to me). They don't even bother to compensate for changes in screen orientation. With a progress dialog popup and ongoing status messages, users will be less inclined to cancel.
Drive has a quirk (as mentioned in an earlier post) of keeping track of files with an internal ID rather than the file name. Multiple writes of the same file name results in multiple files with the same name. Rather than confusing the user (and the macro that adds the records to the spreadsheet) I had to get hold of that file ID, save it to SharedPreferences, and when time came for rewriting, write to that ID. And, if the user had happened to trash it, fish it out of the trash.
So the logic goes: if you have an ID previously saved in SharedPreferences, and that file exists, you're good to go and overwrite. (And fish it out of the trash, whether or not it's in trash, because it's not super easy to tell if it's in trash.) If you don't have a saved ID, or you do but the file isn't there, you have to create a new file (and after it's written to Drive, get its file ID and save it).
Here is a list of the methods to work with Drive files. There is also some sample code at the end of each method's page.
One weirdness about the Drive Quickstart sample code: for reasons unknown, it asks for full access to all the files on the Drive.
credential = GoogleAccountCredential.usingOAuth2(this, DriveScopes.DRIVE);
It doesn't need this, and your users will be O_o if your app asks for the same permission. You really want your app to access only the files it creates:
credential = GoogleAccountCredential.usingOAuth2(this, DriveScopes.DRIVE_FILE);
Lastly, the boyfriend's phone seemed to need this added to the manifest:
<uses-permission android:name="android.permission.USE_CREDENTIALS"/>
I'm not sure if this was due to his Android version, or that he has more than one account registered on the phone, or that he has two-stage authentication.
The sample code, Drive Quickstart, starts up the built-in camera app, grabs the Uri of the saved image, and uploads it to Drive. In the process it needs to get a login name and authorization to write to Drive. Since life is uncertain, it uses startActivityForResult, which returns a result code to determine the next action. I needed significantly more complicated behavior for writing the file, but this is the basic sequence of events.
My application stores the user login (e.g. "mightyjoeyoung@gmail.com") in SharedPreferences, so once it's set, it won't ask again. There are some initial checks if a) a stored name exists and b) the stored name actually associates with a valid account. If it finds nothing, it starts an account picker Intent.
app = (App) getApplication();
// check for a previously saved login
settings = getSharedPreferences(app.PREFS_NAME, 0);
String savedLogin = settings.getString("savedlogin", "");
if (!savedLogin.equals("")) {
app.credential.setSelectedAccountName(savedLogin);
service = getDriveService(app.credential);
}
if (savedInstanceState != null) {
inProgress = savedInstanceState.getBoolean("inProgress");
}
if (app.credential.getSelectedAccount() == null) {
startActivityForResult(app.credential.newChooseAccountIntent(), REQUEST_ACCOUNT_PICKER);
}
When a valid account is set, it authorizes and creates a Drive service object
Drive service = new Drive.Builder(AndroidHttp.newCompatibleTransport(), new GsonFactory(), credential).build();
which is used for the reading/writing transactions. Failure to authorize results in going back to the account picker.
If all is well, it grabs the Uri of the file on the phone (which was created before starting the whole process) and attempts to upload it.
Drive Quickstart's sample code has no UI beyond the bare minimum. While it's busy trying to upload, by all appearances the app has hung. And the first time it's run, it can take up to a minute to complete the process (or at least that's what happened to me). They don't even bother to compensate for changes in screen orientation. With a progress dialog popup and ongoing status messages, users will be less inclined to cancel.
Drive has a quirk (as mentioned in an earlier post) of keeping track of files with an internal ID rather than the file name. Multiple writes of the same file name results in multiple files with the same name. Rather than confusing the user (and the macro that adds the records to the spreadsheet) I had to get hold of that file ID, save it to SharedPreferences, and when time came for rewriting, write to that ID. And, if the user had happened to trash it, fish it out of the trash.
So the logic goes: if you have an ID previously saved in SharedPreferences, and that file exists, you're good to go and overwrite. (And fish it out of the trash, whether or not it's in trash, because it's not super easy to tell if it's in trash.) If you don't have a saved ID, or you do but the file isn't there, you have to create a new file (and after it's written to Drive, get its file ID and save it).
Here is a list of the methods to work with Drive files. There is also some sample code at the end of each method's page.
One weirdness about the Drive Quickstart sample code: for reasons unknown, it asks for full access to all the files on the Drive.
credential = GoogleAccountCredential.usingOAuth2(this, DriveScopes.DRIVE);
It doesn't need this, and your users will be O_o if your app asks for the same permission. You really want your app to access only the files it creates:
credential = GoogleAccountCredential.usingOAuth2(this, DriveScopes.DRIVE_FILE);
Lastly, the boyfriend's phone seemed to need this added to the manifest:
<uses-permission android:name="android.permission.USE_CREDENTIALS"/>
I'm not sure if this was due to his Android version, or that he has more than one account registered on the phone, or that he has two-stage authentication.
Wednesday, May 1, 2013
Well, scratch that
I still don't know if I want to put it in the Play Store or not, but I restored (since that's what I had it doing before we decided to use Drive) a Share button. That brings up the generic dialog to send a text file: your choice of Bluetooth, Copy to clipboard, Drive, Email, Gmail, Google, G+, and Text message. So it doesn't require Drive and the Drive spreadsheet macro, and the user is free to pop the data into their own spreadsheet or database. It's just a CSV file.
And yes, I did make a steampunk scale. I photographed the top surface of a Deco vanity, mushed it into the right proportions, and stuck on Googled images of brass gears and wood borders and such.
However, There are semitransparent XML borders around the weight "dials" and the Save button (see previous post, updated to include the new Notes field and the changes mentioned above), and I haven't coded to compensate for the significantly darker color of the wood scale.The font color is that not-quite-black of Android text, and though the digits are nicely visible, Save is not. It looks, in a word, awful. So I'm not posting the pic.
The ideal thing would be to have a semitransparent white for the Save button and remove the border entirely for the dials. We'll just have to see how easy that is to do programmatically.
And yes, I did make a steampunk scale. I photographed the top surface of a Deco vanity, mushed it into the right proportions, and stuck on Googled images of brass gears and wood borders and such.
However, There are semitransparent XML borders around the weight "dials" and the Save button (see previous post, updated to include the new Notes field and the changes mentioned above), and I haven't coded to compensate for the significantly darker color of the wood scale.The font color is that not-quite-black of Android text, and though the digits are nicely visible, Save is not. It looks, in a word, awful. So I'm not posting the pic.
The ideal thing would be to have a semitransparent white for the Save button and remove the border entirely for the dials. We'll just have to see how easy that is to do programmatically.
Wednesday, April 24, 2013
victory declared
Well, it seemed poised to get into the endless-polishing stage, but I think I'm pretty much done with the weight app. (Although I would kind of like to draw a steampunk scale.)
This has pretty specific requirements. You need to have a Drive account and you need to have a spreadsheet with the macro installed. It is also designed on the assumption that it is used to record a single weight at a time. For these reasons I won't be putting this in the Play Store.
This has pretty specific requirements. You need to have a Drive account and you need to have a spreadsheet with the macro installed. It is also designed on the assumption that it is used to record a single weight at a time. For these reasons I won't be putting this in the Play Store.
![]() |
| Main screen. It will remember the last weight you entered. |
![]() |
| Change background image |
![]() |
| Of course, the enter date can be changed anytime |
![]() |
| List of weights and upload button. Individual weights can be deleted by swiping away. The menu option lets you delete all the weights. |
![]() |
| If you haven't signed in before, it will bring up a picker to choose your Google account. It will remember the account for all subsequent uploads. |
![]() |
| The Google Spreadsheet uses a macro written to find LatestWeights.txt and add its records to the sheet. |
Monday, April 22, 2013
concerns about piracy
Update (5/10/13): Google took this pirate down and I feel gratified. He may just pop up somewhere else, but he'll have to pay the developer registration fee again. Twenty-five dollars may not seem like much, but it offsets some of the ad revenue, which I've heard is pretty tiny. Heck, my friend's book has been up for over 4 months and our combined profit hasn't hit that yet.
I was browsing Play for a game to download, when I came across this one. I thought it looked familiar, downloaded it and started playing. Sure enough, it was this Flash game.
The Android version had a fair number of ads and started to annoy me, and I knew I'd played it before, so I deinstalled it and took a peek at this "developer's" other offerings. There were a few Japanese games, a bunch of games in English. All different styles of artwork. Then I came across one, then two, that had been written by a friend.
I had already seen this friend had ported a couple of his Flash games to Android under his own name. This so-called "developer" was pirating other people's Flash games and posting them under his name, presumably for the ad revenue.
I looked up the Flash versions of several games and contacted four developers. One didn't respond, and the other three said they had not given permission to this guy to port their game. I gave them the link to complain to Google about copyright violation. Hopefully this guy will get taken down.
But this leads me to ask what protections I have as a Play developer? If someone installs my app, how easy would it be for them to take the app from their file system, load it into their IDE and add code to pull in ad networks, and push it back out to the Play store as their own?
Google recently won a case against Viacom wherein its subsidiary YouTube was found to be protected by the DMCA for hosting Viacom TV shows uploaded by its users.
Google also protects copyright in other cases, such as copyrighted music being used in user videos. It has, as far as I understand, a fairly sophisticated algorithm to search for and match to copyrighted material.
Is the same effort put into protecting people who may not even know their work has been ported to Android? Or at least work that already exists at the Play Store and is pirated into another Play Store app? Or is it up to developers to constantly search for theft of their own work?
I was browsing Play for a game to download, when I came across this one. I thought it looked familiar, downloaded it and started playing. Sure enough, it was this Flash game.
The Android version had a fair number of ads and started to annoy me, and I knew I'd played it before, so I deinstalled it and took a peek at this "developer's" other offerings. There were a few Japanese games, a bunch of games in English. All different styles of artwork. Then I came across one, then two, that had been written by a friend.
I had already seen this friend had ported a couple of his Flash games to Android under his own name. This so-called "developer" was pirating other people's Flash games and posting them under his name, presumably for the ad revenue.
I looked up the Flash versions of several games and contacted four developers. One didn't respond, and the other three said they had not given permission to this guy to port their game. I gave them the link to complain to Google about copyright violation. Hopefully this guy will get taken down.
But this leads me to ask what protections I have as a Play developer? If someone installs my app, how easy would it be for them to take the app from their file system, load it into their IDE and add code to pull in ad networks, and push it back out to the Play store as their own?
Google recently won a case against Viacom wherein its subsidiary YouTube was found to be protected by the DMCA for hosting Viacom TV shows uploaded by its users.
Google also protects copyright in other cases, such as copyrighted music being used in user videos. It has, as far as I understand, a fairly sophisticated algorithm to search for and match to copyrighted material.
Is the same effort put into protecting people who may not even know their work has been ported to Android? Or at least work that already exists at the Play Store and is pirated into another Play Store app? Or is it up to developers to constantly search for theft of their own work?
Wednesday, April 10, 2013
The weight project part 2
Eclipse vs IntelliJ and The Google (Drive)
I started programming Java, then Android with IntelliJ some 10 months ago. I like IntelliJ. However, the Google Drive example assumed you were using Eclipse, a popular and also free IDE. It has its own plugin to incorporate Google libraries. I admit I don't understand more than the rudiments of code libraries, so when I started working on the example I could not figure out how to make it work in IntelliJ.
I downloaded Eclipse, and after some struggle with the newest version and learning that it and the Android SDK don't play all so well together, downloaded the next-older version and got it working.
I also learned enough about Eclipse to realize I didn't like it all that much. It's slower, even when you tell it not to automatically compile, and seems delicate. It keeps track of projects in a completely separate folder, so if you delete a file from outside, or rename/move the folder, then try to load/run it, Eclipse has a hissy fit and faints. IntelliJ just looks at what is there and deals with it, even if your project is open. Robust.
I struggled for days trying to add the libraries to IntelliJ the way I was guessing they were being added in Eclipse. (On the Drive example page, there was no description of what the plugin was doing under the hood.) I finally broke down and asked the collective wisdom of StackOverflow.
After some wrangling with a guy who initially didn't seem to understand what I was asking, he built the example himself and presented me with exactly how to add the libraries. Bless this sweet Russian guy's heart. Once I had this, I placed it straight into the weights app and it worked beautifully. Yay!
I started programming Java, then Android with IntelliJ some 10 months ago. I like IntelliJ. However, the Google Drive example assumed you were using Eclipse, a popular and also free IDE. It has its own plugin to incorporate Google libraries. I admit I don't understand more than the rudiments of code libraries, so when I started working on the example I could not figure out how to make it work in IntelliJ.
I downloaded Eclipse, and after some struggle with the newest version and learning that it and the Android SDK don't play all so well together, downloaded the next-older version and got it working.
I also learned enough about Eclipse to realize I didn't like it all that much. It's slower, even when you tell it not to automatically compile, and seems delicate. It keeps track of projects in a completely separate folder, so if you delete a file from outside, or rename/move the folder, then try to load/run it, Eclipse has a hissy fit and faints. IntelliJ just looks at what is there and deals with it, even if your project is open. Robust.
I struggled for days trying to add the libraries to IntelliJ the way I was guessing they were being added in Eclipse. (On the Drive example page, there was no description of what the plugin was doing under the hood.) I finally broke down and asked the collective wisdom of StackOverflow.
After some wrangling with a guy who initially didn't seem to understand what I was asking, he built the example himself and presented me with exactly how to add the libraries. Bless this sweet Russian guy's heart. Once I had this, I placed it straight into the weights app and it worked beautifully. Yay!
Saturday, April 6, 2013
tackling Google Drive
The boyfriend steps on the scale every day, records his weight on the whiteboard, and every once in a while enters those into a Drive spreadsheet, which has a little chart showing his progress. (At 177 +-2, he's quite reasonable, but he feels tracking helps him keep it that way.) So I wrote a little database app for his phone. It saves weights to a comma delimited text file and uploads it to Drive. A macro in the spreadsheet grabs that file and puts the entries at the end of the sheet.
The fun (/sarcasm) part has been learning to upload or update this file on Drive. The API is less than well documented, a lot of the code is compiled so you can't read it in the IDE, and that makes it extra painful since I'm barely versed in these sorts of things. However, there's some nice sample text at the Developers site that show the basics in Java (see "Manage Drive Files").
Interestingly, Drive commands are not so much like a file system as a database. One updates and inserts files instead of copying them. Drive files have an ID, which is a separate creature from the name ("title"). Inserting a file will create a new file, and it could have the same title as another file that already exists -- it won't automatically overwrite it. It's got a different ID, so it must be a different file. So you have to look for the file and grab its ID, and from there you can update it, or do other stuff like delete, change its title, etc.
Drive requests do not ignore the Trash folder, either. During testing, I deleted the uploaded file from Drive, expecting the upload to create a new one, and was frustrated for quite some time because my "new" file wasn't showing up. It had been happily updating the deleted file.
The other thing to watch out for is how much you're trying to do. I followed the Developers sample code to list the files, and it grabs *all* the files, Trash, subfolders, everything. You have to add a query (yeah, database again) to restrict the file listing to where you want it to look, and better yet, the name. After adding the query
request.setQ("'root' in parents and title = 'LatestWeights.txt' and trashed=false");
the whole process speeded up remarkably.
The fun (/sarcasm) part has been learning to upload or update this file on Drive. The API is less than well documented, a lot of the code is compiled so you can't read it in the IDE, and that makes it extra painful since I'm barely versed in these sorts of things. However, there's some nice sample text at the Developers site that show the basics in Java (see "Manage Drive Files").
Interestingly, Drive commands are not so much like a file system as a database. One updates and inserts files instead of copying them. Drive files have an ID, which is a separate creature from the name ("title"). Inserting a file will create a new file, and it could have the same title as another file that already exists -- it won't automatically overwrite it. It's got a different ID, so it must be a different file. So you have to look for the file and grab its ID, and from there you can update it, or do other stuff like delete, change its title, etc.
Drive requests do not ignore the Trash folder, either. During testing, I deleted the uploaded file from Drive, expecting the upload to create a new one, and was frustrated for quite some time because my "new" file wasn't showing up. It had been happily updating the deleted file.
The other thing to watch out for is how much you're trying to do. I followed the Developers sample code to list the files, and it grabs *all* the files, Trash, subfolders, everything. You have to add a query (yeah, database again) to restrict the file listing to where you want it to look, and better yet, the name. After adding the query
request.setQ("'root' in parents and title = 'LatestWeights.txt' and trashed=false");
the whole process speeded up remarkably.
Monday, March 18, 2013
Managed cursors and SimpleCursorAdapter
Are deprecated. RIP. I found a truly excellent example of ContentProvider with SQLite, populating a ListView, here. It's probably a little more complicated than it needs to be, but then again this whole ContentProvider thing seems so needlessly complicated. Another good write-up is here, covering both the old SimpleCursorAdapter and ContentProvider.
But if you want other applications to access the data, you need to use ContentProvider. No turning back now, as they say.
But if you want other applications to access the data, you need to use ContentProvider. No turning back now, as they say.
Sunday, February 10, 2013
Got my first rating on UpGoerFive!
5 stars, yay! Thanks for the lovely comments D'Shan!
Monday, February 4, 2013
Did it again!
A new app, inspired by XKCD comic #1133 for #upgoerfive lovers.
Saw the comic when it was published in January, laughed even though the secret English major in me shuddered at the awfulness of having to use a tiny subset of the language. Then the boyfriend sees the hashtag moving around, sees Splasho's editor, and says, hey, this could be a fun silly thing to write for Android.
This turned into a weekend hackathon and really ended up being a big part of my birthday. We completed early Sunday evening, fell into the endless polishing stage, and got it uploaded to Play late Sunday night.
Get the app here
Saw the comic when it was published in January, laughed even though the secret English major in me shuddered at the awfulness of having to use a tiny subset of the language. Then the boyfriend sees the hashtag moving around, sees Splasho's editor, and says, hey, this could be a fun silly thing to write for Android.
This turned into a weekend hackathon and really ended up being a big part of my birthday. We completed early Sunday evening, fell into the endless polishing stage, and got it uploaded to Play late Sunday night.
Get the app here
Privacy policy? We got 'em here.
August 2026: Bird Calling will be sunsetted in September 2026. The author is considering moving to Kindle. Thank you to everyone who purchased.
Privacy Policy for Apps from Fallinghawks Studio
- This application may log information like your Android version, phone model, and error messages generated by the application.
- We do not collect any personal information about you. In other words, we do not collect information such as your name, street address, phone number or email address. We do not access your contacts list. As part of its sales reports (which we receive for paid apps only), Google Play reports to us your name, city, state, zip code and Android version.
- We do not knowingly collect personal information from children under 13. If you believe we have inadvertently collected such information, please contact us so we can promptly obtain parental consent or remove the information.
- We do not collect or use your precise geographic location.
- There are no methods of seeing what data we collect from you, though if you contact us we'll be happy to show you what we've got on you.
- This application may contain links to sites on the internet, each of which has its own privacy and data collection policies. Google, YouTube, the Play Store, and Blogger are all far more interested in you than am I. I'm just here to try to solve any problems you encounter with my app.
- The Weight Record app stores your login to Google Drive if you use it to upload records to Drive.
- We may keep data indefinitely as part of our debugging records.
- We do not share personally identifiable information (such as name, street address, email or phone) with other companies.
- We do not knowingly allow advertising companies to collect data through our service for ad targeting.
- We take reasonable steps to secure your information against unauthorized access or disclosure. Google Play encrypts transmission of data on pages where you provide payment information. However, no security or encryption method can be guaranteed to protect information from hackers or human error.
- Information we collect may be stored or processed on computers located in any country where we do business.
- We may make identifiable and anonymous information available to third parties in these limited circumstances: (1) with your express consent, (2) when we have a good faith belief it is required by law, (3) when we have a good faith belief it is necessary to protect our rights or property, or (4) to any successor or purchaser in a merger, acquisition, liquidation, dissolution or sale of assets. Your consent will not be required for disclosure in these cases, but we will attempt to notify you, to the extent permitted by law to do so.
- This privacy policy was last updated on 2/8/2017. Our privacy policy may change from time to time. If we make any significant changes to our policies, we will place a prominent notice on our website or application.
Labels:
android,
BirdCalling,
java,
programming,
weight record
Wednesday, January 30, 2013
troubleshooting the Play store, yikes!
Bird Calling is a pretty specialized app, so we've had few sales so far (sad face). Niki promoted it on FB today and got a few people interested enough to shell out $1.99. One of the users though had a problem with an infinitely long download ... tech support to the rescue ... who, ME?
Yes, me. Who else is going to do it? Former IT manager to the rescue.
So I ask him if he's downloading via Wifi connection or his data plan. Data plan.
Does he have any other problems with the data plan, like is it slow to get Maps. No.
Can he download another large app from Play? I find a free 58 MB game called Factory96, ask him to download. It doesn't work either. (Whew. Not my app.)
I tell him that Wifi is a much more stable data connection, and can he connect to his Wifi network and try again.
A few minutes later... SUCCESS. Yay!
Yes, me. Who else is going to do it? Former IT manager to the rescue.
So I ask him if he's downloading via Wifi connection or his data plan. Data plan.
Does he have any other problems with the data plan, like is it slow to get Maps. No.
Can he download another large app from Play? I find a free 58 MB game called Factory96, ask him to download. It doesn't work either. (Whew. Not my app.)
I tell him that Wifi is a much more stable data connection, and can he connect to his Wifi network and try again.
A few minutes later... SUCCESS. Yay!
Thursday, January 24, 2013
at the Play Store
Searches:
bird calling - Angry Birds Star Wars tops a list of over 1000 hits; of course my app is nowhere to be seen.
nicole bird calling - my app plus two "zombie solitaire" games
perretta - my app plus two others
nicole bird - my app plus five or six others
nicole calling - my app close to the top of ~35 others
bird call lady - 0 app results
"bird calling" and "bird call lady" (in quotes) return only my app
I'm not displeased. Those who don't spell Perretta correctly can still find it with Nicole. The thing I find quirky is how bird call lady without quotes brings up nothing, but with quotes, it finds it (it's a phrase in the description). I want it to be findable by people like your mom, or my mom, or anyone who doesn't really get that UI has certain common patterns. You know, the type who would have been better off with an iPhone ;) .
[A few years back I had a sort-of-boyfriend who had a Mac laptop. I know, the finest of programmers use them these days now that they're Unix based, but I'm from the era where Macs' main selling point was their friendliness to the non-technical. Watching this boyfriend browse the internet was an exercise in self-restraint against yelling "NO NO NO NO DON'T CLICK that spammy link," or expecting to peruse a website in an organized manner. When you're using a computer (or anything with a lot of visuals, like an airplane) for the first time, you have a lot to look at and have no idea what's important and what's ignoreable. After a while, most people learn to discern what's important, but some don't -- to them, the computer screen is a Jackson Pollock painting where every place is equally important, and they click on whatever is in front of them. That's the kind of end user I'm talking about.]
And one last bug got eradicated: I'm glad one of my beta testers had a Samsung Galaxy S3. Despite the normal-sized screen, it's xhdpi and from what I could gather from here and here, the S3 uses about 4 times as much memory for an image than other devices.
And naturally, after that bug was done I forgot to include the typo correction in the database and forgot to add the name of the beta tester who found the typo. Hence version 1.0.2.
The uploading of a revision is a bit mystifying. It gives this weird notification about how people who might download v1.0.0 would now be getting v1.0.2, and it gives the impression this is a bad thing. It isn't entirely clear that you must archive the old version first in order to activate the new version, then you additionally have to publish the new version. I'm sure there is reasoning behind this, but it's not the same as my own.
bird calling - Angry Birds Star Wars tops a list of over 1000 hits; of course my app is nowhere to be seen.
nicole bird calling - my app plus two "zombie solitaire" games
perretta - my app plus two others
nicole bird - my app plus five or six others
nicole calling - my app close to the top of ~35 others
bird call lady - 0 app results
"bird calling" and "bird call lady" (in quotes) return only my app
I'm not displeased. Those who don't spell Perretta correctly can still find it with Nicole. The thing I find quirky is how bird call lady without quotes brings up nothing, but with quotes, it finds it (it's a phrase in the description). I want it to be findable by people like your mom, or my mom, or anyone who doesn't really get that UI has certain common patterns. You know, the type who would have been better off with an iPhone ;) .
[A few years back I had a sort-of-boyfriend who had a Mac laptop. I know, the finest of programmers use them these days now that they're Unix based, but I'm from the era where Macs' main selling point was their friendliness to the non-technical. Watching this boyfriend browse the internet was an exercise in self-restraint against yelling "NO NO NO NO DON'T CLICK that spammy link," or expecting to peruse a website in an organized manner. When you're using a computer (or anything with a lot of visuals, like an airplane) for the first time, you have a lot to look at and have no idea what's important and what's ignoreable. After a while, most people learn to discern what's important, but some don't -- to them, the computer screen is a Jackson Pollock painting where every place is equally important, and they click on whatever is in front of them. That's the kind of end user I'm talking about.]
And one last bug got eradicated: I'm glad one of my beta testers had a Samsung Galaxy S3. Despite the normal-sized screen, it's xhdpi and from what I could gather from here and here, the S3 uses about 4 times as much memory for an image than other devices.
And naturally, after that bug was done I forgot to include the typo correction in the database and forgot to add the name of the beta tester who found the typo. Hence version 1.0.2.
The uploading of a revision is a bit mystifying. It gives this weird notification about how people who might download v1.0.0 would now be getting v1.0.2, and it gives the impression this is a bad thing. It isn't entirely clear that you must archive the old version first in order to activate the new version, then you additionally have to publish the new version. I'm sure there is reasoning behind this, but it's not the same as my own.
Tuesday, January 15, 2013
It's done and live!
Here's my app:
"Bird Calling" by Nicole Perretta
I had been waiting for my beta testers to get back to me on the "final" version, and I'm glad I did, because one of them found a bug. The pictures are pretty large (sized for a 7" tablet), and each time you launch an Intent (i.e. go to another bird by clicking rather than swiping) the picture goes to the back stack and stays in memory. We had already done some work to shrink pictures for smaller/older devices, but the back stack was killing even the Nexus 7 within about 5 or 6 back stack additions. We changed the code to recycle the bitmap sooner rather than later.
"Bird Calling" by Nicole Perretta
I had been waiting for my beta testers to get back to me on the "final" version, and I'm glad I did, because one of them found a bug. The pictures are pretty large (sized for a 7" tablet), and each time you launch an Intent (i.e. go to another bird by clicking rather than swiping) the picture goes to the back stack and stays in memory. We had already done some work to shrink pictures for smaller/older devices, but the back stack was killing even the Nexus 7 within about 5 or 6 back stack additions. We changed the code to recycle the bitmap sooner rather than later.
Thursday, January 10, 2013
RelativeLayout for dummies
When I started working with Android I was doing strictly LinearLayouts, but they get complicated and ugly very fast. It's analogous to rowspans and colspans in HTML tables, only messier. Then I learned about RelativeLayout.
It's slick in that you can declare all your elements in (pretty much) a single RelativeLayout instead of having a zillion child LinearLayouts. All you have to do is say where an element should appear in relation to another element. To do this you have
layout_alignParentRight, ~Top, ~Left and ~Bottom to anchor your element to the layout itself. These are ="true" (or not declared at all, I suppose).
layout_toLeftOf and ~RightOf, and layout_above and ~below for positioning relative to other elements. The argument is the id of the other element, e.g. ="@+id/titleBox"
Things learned: the LinearLayout urge is to declare elements in order of appearance from top to bottom. Don't do it that way. Organize by items that are going to stay a fixed size and location versus items that you want to stretch to fill the remaining space.
So I'm laying out elements for a basic media control. It has a TextView for the title of the clip, a ProgressBar, an ImageButton that will later be coded to toggle between play and pause images, and two more TextViews, one to display the total length of the clip and one for the current time position.
I want the title on the left, the play/pause button on the right, and the progress bar to stretch to fill any space in between.
So the first two things I declare are the title and the button.
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="@color/offwhite">
<ImageButton
android:id="@+id/btnPlay"
android:src="@drawable/playbutton"
android:layout_alignParentRight="true"
android:scaleType="fitEnd"
android:layout_width="70dp"
android:layout_height="70dp"
android:paddingRight="10dp"
android:background="@null"
/>
<TextView
android:id="@+id/songTitle"
android:textColor="@color/d_gray"
android:textSize="20dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:paddingLeft="10dp"
android:paddingTop="22dp"
android:text="clip title"
/>
It's slick in that you can declare all your elements in (pretty much) a single RelativeLayout instead of having a zillion child LinearLayouts. All you have to do is say where an element should appear in relation to another element. To do this you have
layout_alignParentRight, ~Top, ~Left and ~Bottom to anchor your element to the layout itself. These are ="true" (or not declared at all, I suppose).
layout_toLeftOf and ~RightOf, and layout_above and ~below for positioning relative to other elements. The argument is the id of the other element, e.g. ="@+id/titleBox"
Things learned: the LinearLayout urge is to declare elements in order of appearance from top to bottom. Don't do it that way. Organize by items that are going to stay a fixed size and location versus items that you want to stretch to fill the remaining space.
So I'm laying out elements for a basic media control. It has a TextView for the title of the clip, a ProgressBar, an ImageButton that will later be coded to toggle between play and pause images, and two more TextViews, one to display the total length of the clip and one for the current time position.
I want the title on the left, the play/pause button on the right, and the progress bar to stretch to fill any space in between.
So the first two things I declare are the title and the button.
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="@color/offwhite">
<ImageButton
android:id="@+id/btnPlay"
android:src="@drawable/playbutton"
android:layout_alignParentRight="true"
android:scaleType="fitEnd"
android:layout_width="70dp"
android:layout_height="70dp"
android:paddingRight="10dp"
android:background="@null"
/>
<TextView
android:id="@+id/songTitle"
android:textColor="@color/d_gray"
android:textSize="20dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:paddingLeft="10dp"
android:paddingTop="22dp"
android:text="clip title"
/>
Then we'll jam the seekbar in between, with a little padding to give it some distance from the other elements.
<SeekBar
android:id="@+id/songProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toLeftOf="@+id/btnPlay"
android:layout_toRightOf="@+id/songTitle"
android:paddingTop="20dp"
android:paddingLeft="15dp"
android:paddingRight="15dp"
/>
The last two textviews are anchored to the seekbar itself. They have the same left and right padding as the seekbar.
<TextView
android:id="@+id/songCurrentDurationLabel"
android:textColor="@color/d_gray"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/songProgressBar"
android:layout_below="@+id/songProgressBar"
android:paddingLeft="15dp"
android:text="current time"
/>
<TextView
android:id="@+id/songTotalDurationLabel"
android:textColor="@color/d_gray"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignRight="@+id/songProgressBar"
android:layout_below="@+id/songProgressBar"
android:paddingRight="15dp"
android:text="total time"
/>
This is so much cleaner than having about 3 nested levels of LinearLayouts.
Subscribe to:
Posts (Atom)






