I'm wondering if there are any good design pattern for initializing custom views (for example passing in a model/renderer). In particular I'm interested in patterns that allow for Android Studio layout preview to work (or something like mirror to work).
For example take a simple game structure:
layout.xml
<LinearLayout>
<com.package.GameSurface>
</LinearLayout>
GameSurface.java
public class GameSurface extends View {
int mComputedParam;
Renderer mRenderer;
GameState mGameState;
public PlaySurface(Context context, AttributeSet attrs) {
super(context, attrs);
}
public PlaySurface(Context context) {
super(context);
}
// What are other ways of achieving this?
public void init(GameState gameState, Renderer renderer) {
mGameState = gameState;
mRenderer = renderer;
}
#Override
protected void onSizeChanged(int newWidth, int newHeight, int oldWidth, int oldHeight) {
// Game state is used to calculate some layout/size/render information on the fly
mComputedParam = (newWidth - getPaddingLeft() - getPaddingRight()) / mGameState.level();
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Render can be swapped out based on level or user settings, etc
mRenderer.draw();
}
}
Something like this will crash the Android studio layout preview since onDraw and onSizeChanged both throw null pointer exceptions if called without the init().
Would there be some other approach that makes this possible? Some ideas off the top of my head:
Have a default renderer and model to fallback to in the layout preview. If this is a preferred solution, is there a way to detect that my code is being run in the layout preview?
Have an option to choose the renderer via some sort of XML attribute, e.g. custom:renderer="#values/default_renderer" and then specify one for layout preview view tools:renderer="#values/preview_renderer". It seems like there might not be good support for this (see Limitations section). This also might not work in some situations where instantiating the renderer couldn't easily be done in a static/preview context.
Perhaps there's a way to run some setup code in the layout preview that I've overlooked?
Related
I am in the process of making a custom view that is essentially an ImageButton with added logic so it also have the behavior of a RadioButton. All I want to do is have it built into the view that when the user clicks the button the image is changed, an internal boolean is marked true to note it is selected, and an interface method is called to let the RadioGroup it is a part of to unselect all the other views within it. I don't want to impact the existing behavior of the base ImageButton whatsoever.
I've only made one other custom view before and that was by following a tutorial almost exactly to the letter and since there are so many different methods inhereted from View that deal with clicks/touches (i.e. onTouch, onClick, motion event, etc.) taking it all in has left me a bit confused. I am fine writing the interface itself, its the modification of ImageButton where I'm not too sure how to attack it.
So, I ask you all: What method/methods do I need to override to add this simple functionality, while not impacting the current behavior of ImageButton, nor screwing up the ability to set an onTouchListener for the button that will perform additional actions on click without compromising this built in radio button logic? If I need to override something that will mess with the default behavior I mentioned, what do I need to put in the new method to restore that functionality?
This is what I have so far:
public class RadioImageButton extends AppCompatImageButton implements RadioCheckable {
//Default constructor
public RadioImageButton(Context context) {
super(context);
initView();
}
//Constructor with defined attributes
public RadioImageButton(Context context, AttributeSet attrs) {
super(context, attrs);
parseAttributes();
initView();
}
//Constructor with defined attributes and attributes taken from style defaults that aren't defined
public RadioImageButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
//=========================================================================
// Setup
//=========================================================================
private void initView()
{
}
private void parseAttributes()
{
}
}
The approach I would like to take would be something like:
...All other code I already showed
mChecked = false;
#Overide
void onClick(...)
{
mChecked = true;
setImageSource(R.example.checked_image); // Or I can use a selector resource
*Call to Radio Interface*;
mOnTouchListener.onTouch(v, event); //Handle user onTouchListener
}
...
and leave all the other code alone, though I'm sure it isn't quite that simple.
I thought a good start would be trying to find the source code for the default ImageButton class and set mine up to be a near replica so I can understand how it works and then modify from there, but all I could really find was this:
https://android.googlesource.com/platform/frameworks/base/+/android-7.0.0_r35/core/java/android/widget/ImageButton.java
and there is no way that is the actual source because pressing Ctrl+O shows many more functions that ImageButton defines that are not inherited from another class; regardless, that link is not at all helpful as its basically a giant comment with little to no code.
Thanks for any suggestions that will help me accomplish this in the most straightforward way.
EDIT: #pskink - Looking through the code you provided, it seems like it is trying to generate a matrix in order to transform the provided drawable (src) so that it fits into a new rectangle (dst) while maintaining the aspect ratio and positioning (hence ScaleToFit.CENTER). I would assume the destination rectangle would be the bounds of the view the drawable is contained in, which in this case is the RadioButton, but while stepping through the override of the "draw()" method it doesn't quite seem to be doing that, though I'm not quite sure how cavas.concat(matrix) is resolved so I'm not positive. Regardless it doesn't seem to work as intended or I am somehow using it wrong.
While maybe not the most robust method, it seems like the most straightforward, yet effective way to handle what I wanted to do was to leverage the Matrix class and its powerful scaling/transformation tools, specifically "setRectToRect()". Creating a custom view that extends RadioButton instead of ImageButton allowed me to make use of the existing RadioGroup, while manipulating characteristics of the button's drawables in the new classes Constructor achieved the behavior I was looking for.
Custom RadioButton class:
public class RadioImageButton extends android.support.v7.widget.AppCompatRadioButton {
int stateDrawable; //Resource ID for RadioButton selector Drawable
D scaledDrawable; //Post-scaling drawable
public RadioImageButtonTwo(Context context) {
super(context);
initView();
}
public RadioImageButtonTwo(Context context, AttributeSet attrs) {
super(context, attrs);
parseAttributes(attrs);
initView();
}
private void parseAttributes(AttributeSet attrs)
{
TypedArray styledAttrs = getContext().obtainStyledAttributes(attrs,R.styleable.RadioImageButtonTwo);
try {
// Obtain selector drawable from attributes
stateDrawable = styledAttrs.getResourceId(R.styleable.RadioImageButtonTwo_button_sDrawable, R.drawable.test_draw2);
} finally {
styledAttrs.recycle(); //Required for public shared view
}
}
private void initView()
{
scaledDrawable = new D(getResources(),stateDrawable); // Create scaled drawable
setBackground(scaledDrawable); // Apply scaled drawable
setButtonDrawable(android.R.color.transparent); // "Disable" button graphic
}
}
See more on setting up a custom view here: https://developer.android.com/training/custom-views/create-view#customattr
Custom drawable class "D" that includes fitCenter scaling thanks to #pskink:
class D extends StateListDrawable {
private Rect bounds = new Rect();
private RectF src = new RectF();
private RectF dst = new RectF();
private Matrix matrix = new Matrix();
public D(Resources r, int resId) {
try {
XmlResourceParser parser = r.getXml(resId);
int type;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
if (type == XmlPullParser.START_TAG && parser.getName().equals("selector")) {
inflate(r, parser, Xml.asAttributeSet(parser));
break;
}
}
} catch (XmlPullParserException | IOException e) {
e.printStackTrace();
}
}
#Override
public void draw(Canvas canvas) {
Drawable current = getCurrent();
bounds.set(0, 0, current.getIntrinsicWidth(), current.getIntrinsicHeight());
current.setBounds(bounds);
src.set(bounds);
dst.set(getBounds());
matrix.setRectToRect(src, dst, Matrix.ScaleToFit.CENTER);
canvas.concat(matrix);
super.draw(canvas);
}
}
Note that for whatever reason setting the button drawable itself to this custom drawable breaks the scaling, so changing the background to the custom drawable and setting the button drawable to transparent was the only way this worked. This custom drawable could easily be expanded upon to have more scaling type options and another view attribute could be defined to allow the user to choose the scaling type through XML.
This custom ImageView that mimics the (pointed out by pskink aswell) could also prove helpful in this task, as it too utilizes the Matrix class to implement multiple types of image scaling: https://github.com/yqritc/Android-ScalableImageView
How do you make a horizontally scrolling ListView for vertical Mongolian script in Android apps?
Background
Android has fairly good support for many of the world's languages, even RTL languages like Arabic and Hebrew. However, there is no built in support for top-to-bottom languages like traditional Mongolian (which is still very much alive in Inner Mongolia and not to be confused with Cyrillic Mongolian). The following graphic shows the text direction with English added for clarity.
Since this functionality is not built into Android, it makes almost every single aspect of app development extremely difficult. This is expecially true with horizontal ListViews, which are not supported out of the box in Android. There is also very, very little information available online. There are a number of app developers for traditional Mongolian, but whether it is for commercial reasons or otherwise, they do not seem to make their code open source.
Because of these difficulties I would like to make a series of StackOverflow questions that can serve as a central place to collect answers for some of the more difficult programming problems related to traditional Mongolian app development. Even if you are not literate in Mongolian, your help reviewing the code, making comments and questions, giving answers or even up-voting the question would be appreciated.
Mongolian Horizontally Scrolling ListView with Vertical Script
A Mongolian ListView needs to have the following requirements:
Scrolls horizontally from left to right
Touch events work the same as with a normal ListView
Custom layouts are supported the same as in a normal ListView
Also needs to support everything that a Mongolian TextView would support:
Supports a traditional Mongolian font
Displays text vertically from top to bottom
Line wrapping goes from left to right.
Line breaks occur at a space (same as English)
The image below shows the basic functionality a Mongolian ListView should have:
My answer is below, but I welcome other ways of solving this problem.
Other related questions in this series:
How to make a traditional Mongolian script TextView in Android
How to make a traditional Mongolian script EditText in Android
More to come... (Toast, Dialog, Menu)
iOS:
How do you make a vertical text UILabel and UITextView for iOS in Swift?
Update
RecyclerViews have a horizontal layout. So it is relatively easy to put a Vertical Mongolian TextView inside one of these. Here is an example from mongol-library.
See this answer for a general solution to using a RecyclerView to make a horizontally scrolling list.
Old answer
It is quite unfortunate that horizontal ListViews are not provided by the Android API. There are a number of StackOverflow Q&As that talk about how to do them, though. Here are a couple samples:
How can I make a horizontal ListView in Android?
Horizontal ListView in Android?
But when I actually tried to implement these suggestions as well as incorporate Mongolian vertical text, I was having a terrible time. Somewhere in my search I found a slightly different answer. It was a class that rotated an entire layout. It did so by extending ViewGroup. In this way anything (including a ListView) can be put in the ViewGroup and it gets rotated. All the touch events work, too.
As I explained in my answer about Mongolian TextViews, it is not enough to simply rotate Mongolian text. That would be enough if every ListView item (or other text element in the ViewGroup) was only a single line, but rotating multiple lines make the line wrap go the wrong direction. However, mirroring the layout horizontally and also using a vertically mirrored font can overcome this, as is shown in the following image.
I adapted the rotated ViewGroup code to also do the horizontal mirroring.
public class MongolViewGroup extends ViewGroup {
private int angle = 90;
private final Matrix rotateMatrix = new Matrix();
private final Rect viewRectRotated = new Rect();
private final RectF tempRectF1 = new RectF();
private final RectF tempRectF2 = new RectF();
private final float[] viewTouchPoint = new float[2];
private final float[] childTouchPoint = new float[2];
private boolean angleChanged = true;
public MongolViewGroup(Context context) {
this(context, null);
}
public MongolViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
setWillNotDraw(false);
}
public View getView() {
return getChildAt(0);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final View view = getView();
if (view != null) {
measureChild(view, heightMeasureSpec, widthMeasureSpec);
setMeasuredDimension(resolveSize(view.getMeasuredHeight(), widthMeasureSpec),
resolveSize(view.getMeasuredWidth(), heightMeasureSpec));
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
#Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
if (angleChanged) {
final RectF layoutRect = tempRectF1;
final RectF layoutRectRotated = tempRectF2;
layoutRect.set(0, 0, right - left, bottom - top);
rotateMatrix.setRotate(angle, layoutRect.centerX(), layoutRect.centerY());
rotateMatrix.postScale(-1, 1);
rotateMatrix.mapRect(layoutRectRotated, layoutRect);
layoutRectRotated.round(viewRectRotated);
angleChanged = false;
}
final View view = getView();
if (view != null) {
view.layout(viewRectRotated.left, viewRectRotated.top, viewRectRotated.right,
viewRectRotated.bottom);
}
}
#Override
protected void dispatchDraw(Canvas canvas) {
canvas.save();
canvas.rotate(-angle, getWidth() / 2f, getHeight() / 2f);
canvas.scale(-1, 1);
super.dispatchDraw(canvas);
canvas.restore();
}
#Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
invalidate();
return super.invalidateChildInParent(location, dirty);
}
#Override
public boolean dispatchTouchEvent(MotionEvent event) {
viewTouchPoint[0] = event.getX();
viewTouchPoint[1] = event.getY();
rotateMatrix.mapPoints(childTouchPoint, viewTouchPoint);
event.setLocation(childTouchPoint[0], childTouchPoint[1]);
boolean result = super.dispatchTouchEvent(event);
event.setLocation(viewTouchPoint[0], viewTouchPoint[1]);
return result;
}
}
The Mongolian vertically mirrored font still needs to be set somewhere else, though. I find it easiest to make a custom TextView to do it:
public class MongolNonRotatedTextView extends TextView {
// This class does not rotate the textview. It only displays the Mongol font.
// For use with MongolLayout, which does all the rotation and mirroring.
// Constructors
public MongolNonRotatedTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
public MongolNonRotatedTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public MongolNonRotatedTextView(Context context) {
super(context);
init();
}
// This class requires the mirrored Mongolian font to be in the assets/fonts folder
private void init() {
Typeface tf = Typeface.createFromAsset(getContext().getAssets(),
"fonts/MongolMirroredFont.ttf");
setTypeface(tf);
}
}
Then the custom ListView item xml layout can look something like this:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/rlListItem"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<com.example.MongolNonRotatedTextView
android:id="#+id/tvListViewText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"/>
</RelativeLayout>
Known issues:
If you look carefully at the image below you can see faint horizontal and vertical lines around the text. Although this image comes from another developer's app, I am getting the same artifacts in my app when I use the rotated ViewGroup (but not when I use the rotated TextView). If anyone knows where these are coming from, please leave me a comment!
This solution does not deal with rendering the Unicode text. Either you need to use non-Unicode text (discouraged) or you need to include a rendering engine in your app. (Android does not support OpenType smartfont rendering at this time. Hopefully this will change in the future. iOS, by comparison does support complex text rendering fonts.) See this link for a Unicode Mongolian rendering engine example.
I have a custom View class in Android, which is to display nothing in the beginning, but only draw its content as soon as show() has been called on it.
I've checked this on Android 4.1 and 4.4, and it seems to work. However, some users with Android KitKat (e.g. on a Samsung Galaxy S4), have reported that nothing is ever drawn in this View. What could be the reason for that? (The full code can be found here.)
Usage in XML:
<com.my.package.MyView
android:id="#+id/my_view"
android:layout_width="48dp"
android:layout_height="48dp" />
Usage in Java:
MyView myView = (MyView) findViewById(R.id.my_view);
myView.show(42);
Referenced class MyView in Java:
public class MyView extends View {
public MyView(Context context) {
super(context);
init();
}
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
protected void init() {
setWillNotDraw(true);
}
public void show(int input) {
// do some initialization so that onDraw() knows what to draw
setWillNotDraw(false);
// invalidate() doesn't seem to be necessary (no effect)
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// draw things on the canvas now
}
}
After several more hours of debugging, I finally found the solution.
At first, I added this to my init() method (which is called from all constructors):
setWillNotDraw(false);
Android must know that this View must be rendered and drawn, which should be the default value, anyway. But if you extend ViewGroup, for example, it is not the default setting, so you have to clear that flag.
As others have noted, one should only call that method once, in order to set correct flag for the View. If you want to prevent drawing in the beginning, use a custom flag and only execute the statements in your onDraw(...) if appropriate. Thus I remove the second call to setWillNotDraw(...) from show(...).
In addition to that, I added a call to ...
invalidate();
... at the end of show(...), which is when I provide input to my custom View and it must be re-drawn.
However, the problem of the View not being drawn ("invisible" View) did only occur on certain devices. It was, for example, a Samsung Galaxy S4 (Android 4.4). When I tested it on the emulator with Android 4.4, the problem did not occur. The reason for that was actually simple:
View.isHardwareAccelerated() revealed that the Galaxy S4 (Android 4.4) was using hardware acceleration on the custom View, while the Galaxy S3 Mini (Android 4.1.2), the Galaxy S2 and the emulator were not.
Here's what the docs say about hardware acceleration:
Hardware acceleration is not supported for all of the 2D drawing operations, turning it on might affect some of your custom views or drawing calls. Problems usually manifest themselves as invisible elements, exceptions, or wrongly rendered pixels.
And apart from that, the GPU can cache your custom View as a static image.
The simple solution (in init()) was therefore to disable hardware acceleration for the custom View class:
if (Build.VERSION.SDK_INT >= 11) {
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}
Keep your own boolean around for whether you should be drawing or not:
private boolean mDraw;
protected void init() {
mDraw = false;
}
public void show(int input) {
mDraw = true;
invalidate();
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(!mDraw) return;
// draw things on the canvas now
}
Alternatively, setVisibility when you actually need it:
protected void init() {
setVisibility(View.INVISIBLE);
}
public void show(int input) {
setVisibility(View.VISIBLE);
invalidate();
}
I'm interested in using a custom View to draw, measure, and display a set of buttons that is dependent on a back-end for my application. This requires me to implement this in Android dynamically. Would you help me get started?
Here we go: first in my MainActivity I instantiate my custom class which inherits from TableLayout which is also a view:
var keyboardView = new KeyboardView(this, layout, droidLayout, this.Colors);
Then I set the content view to the fresh instance of my custom class: SetContentView(keyboardView); Here's my class's constructor which just helps me get scope on all of the info I need:
public KeyboardView(Context context, KeyboardLayout layout, int droidLayout, Dictionary<string, int> colors)
: base(context) {
this._Colors = colors;
this._Context = context;
this.KeyboardLayout = layout;
this.SetWillNotDraw(false);
//this.ButtonLayout = ll;
this.DrawingCacheEnabled = true;
this.DroidLayout = droidLayout;
I've also overridden both OnMeasure and OnDraw:
protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int h = 100;
int w = 100;
//Overall keyboard/View dimensions //Difference between Canvas size and KeyboardDimensions?
this.SetMeasuredDimension(w, h);
//this.Layout.CanvasSize.
}
protected override void OnDraw(Canvas canvas) {
var bt = new Button(this._Context);
bt.Text = "laaaa";
this.AddView(bt);
}
Now what's happening is that OnDraw and OnMeasure both get it, in fact, OnDraw seems to be getting hit lots of time -- more so then I wish to count. However, the one button that I added via AddView is NOT drawn on the screen. If you guys could help me get this one button on the screen I can get to writing the core logic!
On a side-note: I can draw stuff on my screen if I set an XML layout file as the view as such: SetContentView(Resource.Layout.LayoutName) But, since the nature of my program requires dynamic views being added all the time, I'd rather avoid writing lengthy Layout files. Thanks guys! Bump my question up if you think it's a worthwhile one!
In general, this is a good place to start. In particular it details how to add custom attributes, how to perform custom drawing, and how to design custom events that make sense in the context of your own application.
Also, it looks like you're trying to use C# style syntax in Java, which won't work for things like inheritance. Reading some java tutorials might help you out.
This is a good resource for that and should help you get up and going. Good luck!
Don't forget to call the super() to allow the parent class to do what it needs to do for the overridden method.
protected override void OnDraw(Canvas canvas) {
super(canvas)
}
Related link
http://developer.android.com/guide/topics/ui/custom-components.html#compound
I'm not sure if this is possible, and I couldn't find a topic based on it, but if it's been answered before drop me a link and that will be that.
What I'm looking to do right now is resize some of the default Android widgets, specifically DatePicker and TimePicker, to use in an Activity. But as far as I can see the only result of modifying the width or height of either Picker (in a negative direction) results in a cropped view, rather than a scaled/stretched view of the widget.
I am open to my own custom widgets of my own, but I would really prefer to keep this project as simple and clean as possible, matching the Android OS UI as much as possible, so using the native DatePicker and TimePicker seems like a logical choice to me. If anyone knows how to scale these widgets down rather than cropping them, I'd really appreciate it.
Thanks.
It is a very bad hack, but it should work:
Create a new view extending LinearLayout, overwrite method getChildStaticTransformation and setStaticTransformationsEnabled explicit to true.
In the method getChildStaticTransformation you can manipulate the tranformation parameter to scale down all the content of your extended LinearLayout.
And then add the DatePicker or something else as a child of this view.
EG:
public class ZoomView
extends LinearLayout
{
private float sf = 1f;
public ZoomView(final Context context, final AttributeSet attrs)
{
super(context, attrs);
setStaticTransformationsEnabled(true);
}
public ZoomView(final Context context)
{
super(context);
setStaticTransformationsEnabled(true);
}
public void setScaling(final float sf)
{
this.sf = sf;
}
#Override
protected boolean getChildStaticTransformation(final View child, final Transformation t)
{
t.clear();
t.setTransformationType(Transformation.TYPE_MATRIX);
final Matrix m = t.getMatrix();
m.setScale(this.sf, this.sf);
return true;
}
}