I am making an Android application using custom views. I have done this many times before but this time I seem to have encountered a weird issue.
When attempting to make the layout, either through XML or the layout editor, my custom view does not layout as expected. When I place one of my views to the right of an existing one, it appears to align to the left. I have attached an image to show what I mean
I'm not certain what is causing this, it doesn't appear to happen with any other view, and has never happened before with previous applications despite me using similar code for the custom view.
Has anyone encountered an issue like this before? If so, any suggestions?
I can put up the code for the view and other potentially relevant code if requested.
EDIT - Alright, coding as requested, hopefully it doesn't offend anyone too much.
XML:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/relativeLayout"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<com.offthemap.sudokusolver.SudokuButton
android:id="#+id/sudokuButton2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true" />
<com.offthemap.sudokusolver.SudokuButton
android:id="#+id/sudokuButton1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_toRightOf="#+id/sudokuButton2" />
</RelativeLayout>
Custom View:
package com.offthemap.sudokusolver;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.View;
public class SudokuButton extends View
{
private static Bitmap background, border, borderSelected;
private static Bitmap[] number;
private static int size = 100;
private int numberIndex = -1;
public SudokuButton(Context context, AttributeSet attrs)
{
super(context);
Resources res = getResources();
if (background == null){background = Bitmap.createScaledBitmap(BitmapFactory.decodeResource(res, R.drawable.background), size, size, false);}
if (border == null){border = Bitmap.createScaledBitmap(BitmapFactory.decodeResource(res, R.drawable.border), size, size, false);}
if (borderSelected == null){borderSelected = Bitmap.createScaledBitmap(BitmapFactory.decodeResource(res, R.drawable.borderselected), size, size, false);}
if (number == null)
{
number = new Bitmap[]{
Bitmap.createScaledBitmap(BitmapFactory.decodeResource(res, R.drawable.one), size, size, false),
Bitmap.createScaledBitmap(BitmapFactory.decodeResource(res, R.drawable.two), size, size, false),
Bitmap.createScaledBitmap(BitmapFactory.decodeResource(res, R.drawable.three), size, size, false),
Bitmap.createScaledBitmap(BitmapFactory.decodeResource(res, R.drawable.four), size, size, false),
Bitmap.createScaledBitmap(BitmapFactory.decodeResource(res, R.drawable.five), size, size, false),
Bitmap.createScaledBitmap(BitmapFactory.decodeResource(res, R.drawable.six), size, size, false),
Bitmap.createScaledBitmap(BitmapFactory.decodeResource(res, R.drawable.seven), size, size, false),
Bitmap.createScaledBitmap(BitmapFactory.decodeResource(res, R.drawable.eight), size, size, false),
Bitmap.createScaledBitmap(BitmapFactory.decodeResource(res, R.drawable.nine), size, size, false)
};
}
}
public void increaseNumber()
{
numberIndex++;
if (numberIndex == 9){numberIndex = -1;}
invalidate();
}
public void setNumber(int newNumber)
{
numberIndex = newNumber;
}
public static void setSize(int newSize)
{
size = newSize;
}
protected void onDraw(Canvas canvas)
{
canvas.drawBitmap(background, 0, 0, null);
if (numberIndex != -1)
{
canvas.drawBitmap(number[numberIndex], 0, 0, null);
}
canvas.drawBitmap(border, 0, 0, null);
}
#Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
}
private int measureWidth(int measureSpec)
{
int preferred = background.getWidth();
return getMeasurement(measureSpec, preferred);
}
private int measureHeight(int measureSpec)
{
int preferred = background.getHeight();
return getMeasurement(measureSpec, preferred);
}
private int getMeasurement(int measureSpec, int preferred)
{
int specSize = MeasureSpec.getSize(measureSpec);
int measurement = 0;
switch(MeasureSpec.getMode(measureSpec))
{
case MeasureSpec.EXACTLY:
measurement = specSize;
break;
case MeasureSpec.AT_MOST:
measurement = Math.min(preferred, specSize);
break;
default:
measurement = preferred;
break;
}
return measurement;
}
}
SECOND EDIT - Appears it was just an issue with using static bitmaps, not sure why it was happening but removing it seemed to fix it.
Can you post your xml where you define the layout_Right_of ?
Sometimes is a problem of if you define before the referenced view or after, i mean where you use de #+id or the #id if its already defined.
-- EDIT --
public class SudokuButton extends View
{
To:
public class SudokuButton extends RelativeLayout
and:
<com.offthemap.sudokusolver.SudokuButton
android:id="#+id/sudokuButton1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_toRightOf="#+id/sudokuButton2" />
To:
<com.offthemap.sudokusolver.SudokuButton
android:id="#+id/sudokuButton1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_toRightOf="#id/sudokuButton2" />
because is already defined in the preview component in the R.
Two comments, try extending from a ViewGroup like Relative or Linear Layout in your custom component, another thing is that you can reference from your second component to the first one without the #+id
Regards,
Alex.
Related
I have used android.support.design.widget.TextInputLayout to make a password input that allows the user to toggle readability on the password. The xml is as follows:
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:hintEnabled="false"
app:passwordToggleDrawable="#drawable/password_toggle_selector"
app:passwordToggleEnabled="true" >
<android.support.design.widget.TextInputEditText
android:id="#+id/password"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:hint="Password"
android:inputType="textPassword"/>
</android.support.design.widget.TextInputLayout>
The drawable selector is as described by How to customize android passwordToggleDrawable
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="#drawable/password_toggle_show"
android:state_checked="true"/>
<item android:drawable="#drawable/password_toggle_hide"/>
</selector>
The issue is that the custom drawable becomes really large. Not larger than the edittext, but rather it seems to maximize its size while still fitting inside it (so, it seems to be bounded by the height of the element). However, if I leave the passwordToggleDrawable property unset, the drawable for the toggle is sized as is normal for android (I am sure you have seen the icon in other apps before). After much searching I have found a way to resize the custom one, but I am not happy with how its done (requires 2 extra xml files per drawable) and it only works for API 23+.
I would like to know if there is a good way to set the size of the drawable, or better yet, make it target the size of the default drawable?
I have tried setting the padding of the EditText as the source of TextInputLayout says that it gets the four paddings from it and apply to the mPasswordToggleView (line 1143), but it made no change on the icon and (as expected) also affected the padding of the EditText. I have tried setting minheight to 0. I have also tried changing between EditText and TextInputEditText (using the latter now as it seems to be recommended). I have tried switching the layout_height properties to wrap_content. I have tried scaling the drawable using xml's <scale> tag with the scale properties set. I have tried similarly with the <inset> tag. But none of those methods works.
The way I found (and am currently using) to resize the drawable that actually works is by using the xml tag <layer-list>, while setting the width and height properties. Then the <selector> xml file references those resized drawables instead of the png ones. But I don't like this solution because as I mentioned it requires API 23 and because of that results in a total of 4 extra xml files. It also sets the width and height by themselves, instead of keeping the ratio locked.
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:drawable="#drawable/password_toggle_hide"
android:width="22dp"
android:height="15dp"/>
</layer-list>
TL;DR
How do I set the size of a custom passwordToggleDrawable in TextInputLayout? Preferably to same size as the default drawable.
I know this is an old question, but I faced the same problem and I believe I figure out a simple solution for this.
I'm using the TextInputLayout for the newest material library, and the only thing that I did was to find the reference for the endIcon from the TextInputLayout and change it's minimum dimensions.
val dimension = //here you get the dimension you want to
val endIconImageView = yourTextInputLayout.findViewById<ImageView>(R.id.text_input_end_icon)
endIconImageView.minimumHeight = dimension
endIconImageView.minimumWidth = dimension
yourTextInputLayout.requestLayout()
Important things to notice:
I did this on the OnFinishedInflated from a custom TextInputLayout, but I believe it will work fine on some activity class.
Cheers!
I face same problem. To avoid this situation I used png and set them based dpi like drawable-hdpi, drawable-mdpi etc. Also make those drawable as per radio. Hope that this tricks also work for you.
I were unable to find any solution to the question I actually asked, but I decided to instead solve the issue by disregarding the "in InputTextLayout" part of the question and implemented my own version of the class.
Mostly it is just a copy of InputTextLayout (sadly that class doesnt translate well for subclassing as everything is private) but with most of the stuff I dont need removed, and more importantly, with the CheckableImageButton mPasswordToggleView changed to a ViewGroup containing a View.
The ViewGroup is the clickable button, and handles setMinimumDimensions to keep the clickable area at min 48 dp, like the original did through design_text_input_password_icon.xml. This also makes small drawables not hug the right side of the screen as they are centered in the clickable area, giving the margin that the default drawable appears to have.
The View (or more precisely, a new subclass of it I called CheckableView) is the actual drawable (setBackground()), replacing the CheckableImageButton as the container of the drawable that lets it switch based on state_checked selector.
The xml-property passwordToggleSize allows a dimension to be set, which is used to scale the drawable. I opted to only have one value instead of width&height, and the drawable scales with its ratio locked such that its greatest dimension matches the dimension specified. I made the default size 24dp, as is specified for the default-drawable in design_ic_visibility.xml.
PasswordToggleLayout.java:
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import android.support.v4.graphics.drawable.DrawableCompat;
import android.support.v4.view.AbsSavedState;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.TextViewCompat;
import android.text.method.PasswordTransformationMethod;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import com.mylifediary.android.client.R;
public class PasswordToggleLayout extends LinearLayout {
// Default values from InputTextLayout's drawable and inflated layout
final int BUTTON_MIN_SIZE = 48; // The button is 48 dp at minimum.
final int DEFAULT_DRAWABLE_SIZE = 24; // The default drawable is 24 dp.
int mButtonMinSize;
final FrameLayout mInputFrame;
EditText mEditText;
private boolean mPasswordToggleEnabled;
private Drawable mPasswordToggleDrawable;
private CharSequence mPasswordToggleContentDesc;
ViewGroup mPasswordToggleViewGroup;
CheckableView mPasswordToggleView;
private boolean mPasswordToggledVisible;
private int mPasswordToggleSize;
private Drawable mPasswordToggleDummyDrawable;
private Drawable mOriginalEditTextEndDrawable;
private ColorStateList mPasswordToggleTintList;
private boolean mHasPasswordToggleTintList;
public PasswordToggleLayout(Context context) {
this(context, null);
}
public PasswordToggleLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PasswordToggleLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setOrientation(VERTICAL);
setWillNotDraw(false);
setAddStatesFromChildren(true);
mButtonMinSize = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, BUTTON_MIN_SIZE,
getResources().getDisplayMetrics());
mInputFrame = new FrameLayout(context);
mInputFrame.setAddStatesFromChildren(true);
addView(mInputFrame);
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.PasswordToggleLayout, defStyleAttr,
R.style.Widget_Design_TextInputLayout);
mPasswordToggleEnabled = a.getBoolean(
R.styleable.PasswordToggleLayout_passwordToggleEnabled, false);
mPasswordToggleDrawable = a.getDrawable(
R.styleable.PasswordToggleLayout_passwordToggleDrawable);
mPasswordToggleContentDesc = a.getText(
R.styleable.PasswordToggleLayout_passwordToggleContentDescription);
if (a.hasValue(R.styleable.PasswordToggleLayout_passwordToggleTint)) {
mHasPasswordToggleTintList = true;
mPasswordToggleTintList = a.getColorStateList(
R.styleable.PasswordToggleLayout_passwordToggleTint);
}
mPasswordToggleSize = a.getDimensionPixelSize(
R.styleable.PasswordToggleLayout_passwordToggleSize,
(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
DEFAULT_DRAWABLE_SIZE, getResources().getDisplayMetrics()));
a.recycle();
applyPasswordToggleTint();
}
private void setEditText(EditText editText) {
// If we already have an EditText, throw an exception
if (mEditText != null) {
throw new IllegalArgumentException(
"We already have an EditText, can only have one");
}
mEditText = editText;
final boolean hasPasswordTransformation = hasPasswordTransformation();
updatePasswordToggleView();
}
private void updatePasswordToggleView() {
if (mEditText == null) {
// If there is no EditText, there is nothing to update
return;
}
if (shouldShowPasswordIcon()) {
if (mPasswordToggleView == null) {
// Keep ratio
double w = mPasswordToggleDrawable.getIntrinsicWidth();
double h = mPasswordToggleDrawable.getIntrinsicHeight();
double scale = mPasswordToggleSize / Math.max(w,h);
int scaled_width = (int) (w * scale);
int scaled_height = (int) (h * scale);
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT,
Gravity.CENTER_VERTICAL | Gravity.END | Gravity.RIGHT);
FrameLayout.LayoutParams lp2 = new FrameLayout.LayoutParams(
scaled_width, scaled_height, Gravity.CENTER);
mPasswordToggleViewGroup = new FrameLayout(this.getContext());
mPasswordToggleViewGroup.setMinimumWidth(mButtonMinSize);
mPasswordToggleViewGroup.setMinimumHeight(mButtonMinSize);
mPasswordToggleViewGroup.setLayoutParams(lp);
mInputFrame.addView(mPasswordToggleViewGroup);
mPasswordToggleViewGroup.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
passwordVisibilityToggleRequested(false);
}
});
mPasswordToggleView = new CheckableView(this.getContext());
mPasswordToggleView.setBackground(mPasswordToggleDrawable);
mPasswordToggleView.setContentDescription(mPasswordToggleContentDesc);
mPasswordToggleView.setLayoutParams(lp2);
mPasswordToggleViewGroup.addView(mPasswordToggleView);
}
if (mEditText != null && ViewCompat.getMinimumHeight(mEditText) <= 0) {
// We should make sure that the EditText has the same min-height
// as the password toggle view. This ensure focus works properly,
// and there is no visual jump if the password toggle is enabled/disabled.
mEditText.setMinimumHeight(
ViewCompat.getMinimumHeight(mPasswordToggleViewGroup));
}
mPasswordToggleViewGroup.setVisibility(VISIBLE);
mPasswordToggleView.setChecked(mPasswordToggledVisible);
// Need to add a dummy drawable as the end compound drawable so that
// the text is indented and doesn't display below the toggle view.
if (mPasswordToggleDummyDrawable == null) {
mPasswordToggleDummyDrawable = new ColorDrawable();
}
// Important to use mPasswordToggleViewGroup, as mPasswordToggleView
// wouldn't replicate the margin of the default-drawable.
mPasswordToggleDummyDrawable.setBounds(
0, 0, mPasswordToggleViewGroup.getMeasuredWidth(), 1);
final Drawable[] compounds = TextViewCompat.getCompoundDrawablesRelative(mEditText);
// Store the user defined end compound drawable so that we can restore it later
if (compounds[2] != mPasswordToggleDummyDrawable) {
mOriginalEditTextEndDrawable = compounds[2];
}
TextViewCompat.setCompoundDrawablesRelative(mEditText, compounds[0],
compounds[1], mPasswordToggleDummyDrawable, compounds[3]);
// Copy over the EditText's padding so that we match
mPasswordToggleViewGroup.setPadding(mEditText.getPaddingLeft(),
mEditText.getPaddingTop(), mEditText.getPaddingRight(),
mEditText.getPaddingBottom());
} else {
if (mPasswordToggleViewGroup != null
&& mPasswordToggleViewGroup.getVisibility() == VISIBLE) {
mPasswordToggleViewGroup.setVisibility(View.GONE);
}
if (mPasswordToggleDummyDrawable != null) {
// Make sure that we remove the dummy end compound drawable if
// it exists, and then clear it
final Drawable[] compounds = TextViewCompat.getCompoundDrawablesRelative(mEditText);
if (compounds[2] == mPasswordToggleDummyDrawable) {
TextViewCompat.setCompoundDrawablesRelative(mEditText,
compounds[0], compounds[1],
mOriginalEditTextEndDrawable, compounds[3]);
mPasswordToggleDummyDrawable = null;
}
}
}
}
private void applyPasswordToggleTint() {
if (mPasswordToggleDrawable != null && mHasPasswordToggleTintList) {
mPasswordToggleDrawable = DrawableCompat.wrap(mPasswordToggleDrawable).mutate();
DrawableCompat.setTintList(mPasswordToggleDrawable, mPasswordToggleTintList);
if (mPasswordToggleView != null
&& mPasswordToggleView.getBackground() != mPasswordToggleDrawable) {
mPasswordToggleView.setBackground(mPasswordToggleDrawable);
}
}
}
private void passwordVisibilityToggleRequested(boolean shouldSkipAnimations) {
if (mPasswordToggleEnabled) {
// Store the current cursor position
final int selection = mEditText.getSelectionEnd();
if (hasPasswordTransformation()) {
mEditText.setTransformationMethod(null);
mPasswordToggledVisible = true;
} else {
mEditText.setTransformationMethod(PasswordTransformationMethod.getInstance());
mPasswordToggledVisible = false;
}
mPasswordToggleView.setChecked(mPasswordToggledVisible);
if (shouldSkipAnimations) {
mPasswordToggleView.jumpDrawablesToCurrentState();
}
// And restore the cursor position
mEditText.setSelection(selection);
}
}
private boolean hasPasswordTransformation() {
return mEditText != null
&& mEditText.getTransformationMethod() instanceof PasswordTransformationMethod;
}
private boolean shouldShowPasswordIcon() {
return mPasswordToggleEnabled && (hasPasswordTransformation() || mPasswordToggledVisible);
}
#Override
public void addView(View child, int index, final ViewGroup.LayoutParams params) {
if (child instanceof EditText) {
// Make sure that the EditText is vertically at the bottom,
// so that it sits on the EditText's underline
FrameLayout.LayoutParams flp = new FrameLayout.LayoutParams(params);
flp.gravity = Gravity.CENTER_VERTICAL
| (flp.gravity & ~Gravity.VERTICAL_GRAVITY_MASK);
mInputFrame.addView(child, flp);
// Now use the EditText's LayoutParams as our own and update them
// to make enough space for the label
mInputFrame.setLayoutParams(params);
setEditText((EditText) child);
} else {
// Carry on adding the View...
super.addView(child, index, params);
}
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
updatePasswordToggleView();
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
#Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
ss.isPasswordToggledVisible = mPasswordToggledVisible;
return ss;
}
#Override
protected void onRestoreInstanceState(Parcelable state) {
if (!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
if (ss.isPasswordToggledVisible) {
passwordVisibilityToggleRequested(true);
}
requestLayout();
}
static class SavedState extends AbsSavedState {
boolean isPasswordToggledVisible;
SavedState(Parcelable superState) {
super(superState);
}
SavedState(Parcel source, ClassLoader loader) {
super(source, loader);
isPasswordToggledVisible = (source.readInt() == 1);
}
#Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeInt(isPasswordToggledVisible ? 1 : 0);
}
public static final Creator<SavedState> CREATOR = new ClassLoaderCreator<SavedState>() {
#Override
public SavedState createFromParcel(Parcel in, ClassLoader loader) {
return new SavedState(in, loader);
}
#Override
public SavedState createFromParcel(Parcel in) {
return new SavedState(in, null);
}
#Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
public static class CheckableView extends View {
private final int[] DRAWABLE_STATE_CHECKED =
new int[]{android.R.attr.state_checked};
private boolean mChecked;
public CheckableView(Context context) {
super(context);
}
public CheckableView(Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
}
public CheckableView(Context context, #Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setChecked(boolean checked) {
if (mChecked != checked) {
mChecked = checked;
refreshDrawableState();
}
}
#Override
public int[] onCreateDrawableState(int extraSpace) {
if (mChecked) {
return mergeDrawableStates(
super.onCreateDrawableState(extraSpace
+ DRAWABLE_STATE_CHECKED.length), DRAWABLE_STATE_CHECKED);
} else {
return super.onCreateDrawableState(extraSpace);
}
}
}
}
And then in an attrs.xml:
<declare-styleable name="PasswordToggleLayout">
<attr name="passwordToggleEnabled" format="boolean"/>
<attr name="passwordToggleDrawable" format="reference"/>
<attr name="passwordToggleContentDescription" format="string"/>
<attr name="passwordToggleTint" format="color"/>
<attr name="passwordToggleSize" format="dimension"/>
</declare-styleable>
Same issue for me. The problem comes from the gradle material API implementation:
implementation 'com.google.android.material:material:1.1.0'
downgrade to version 1.0.0 fixes the issue:
implementation 'com.google.android.material:material:1.0.0'
Im new in Android world. I want to put some parallax background effects in my app.
How can I do it? How to approach to this in Android?
Is there any productive way to create 2-3 layer parallax background? Is there some tool, or class in android API?
Or maybe I have to modify background image location or margins "manually" in code?
Im using API level 19.
I have tried to understand Paralloid library, but this is too big to understand without any explanation. Im new to Android and Java, im not familiar with all Layouts and other UI objects, however I'm familiar with MVC.
I started bounty, maybe someone can explain step by step how that library works.
This is what you can do:
In your activity/fragment layout file specify 2 ScrollView's (say background_sv and content_sv).
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<com.example.parallax.MyScrollView
android:id="#+id/background_sv"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<ImageView
android:id="#+id/parallax_bg"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="..." />
</com.example.parallax.MyScrollView>
<com.example.parallax.MyScrollView
android:id="#+id/content_sv"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
</LinearLayout>
</com.example.parallax.MyScrollView>
</RelativeLayout>
Add a dummy view in the content scrollview of the height of the background and make it transparent. Now, attach a scroll listener to the content_sv. When the content scrollview is scrolled, call
mBgScrollView.scrollTo(0, (int)(y /*scroll Of content_sv*/ / 2f));
The existing API's doesn't have the support to get the scroll events.
Hence, we need to create a Custom ScrollView, to provide the ScrollViewListener.
package com.example.parallax;
// imports;
public class MyScrollView extends ScrollView {
public interface ScrollViewListener {
void onScrollChanged(MyScrollView scrollView, int x, int y, int oldx, int oldy);
}
private ScrollViewListener scrollViewListener = null;
public MyScrollView(Context context) {
super(context);
}
public MyScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public MyScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setScrollViewListener(ScrollViewListener scrollViewListener) {
this.scrollViewListener = scrollViewListener;
}
#Override
protected void onScrollChanged(int x, int y, int oldx, int oldy) {
super.onScrollChanged(x, y, oldx, oldy);
if(scrollViewListener != null) {
scrollViewListener.onScrollChanged(this, x, y, oldx, oldy);
}
}
}
Here is the activity which hosts both the content ScrollView and background ScrollView
package com.example.parallax;
// imports;
public class ParallaxActivity extends Activity implements ScrollViewListener {
private MyScrollView mBgScrollView;
private MyScrollView mContentScrollView;
#Override
public void onCreate(Bundle savedInstanceState) {
mBgScrollView = findViewById(R.id.background_sv);
mContentScrollView = findViewById(R.id.content_sv);
mContentScrollView.setOnScrollListener(this);
}
// this is method for onScrollListener put values according to your need
#Override
public void onScrollChanged(MyScrollView scrollView, int x, int y, int oldx, int oldy) {
super.onScrollChanged(scrollView, x, y, oldx, oldy);
// when the content scrollview will scroll by say 100px,
// the background scrollview will scroll by 50px. It will
// look like a parallax effect where the background is
// scrolling with a different speed then the content scrollview.
mBgScrollView.scrollTo(0, (int)(y / 2f));
}
}
I think the question is unclear, so this is not really an answer so much as an attempt to clarify with more detail than I could include in a comment.
My question is about what kind of parallax effect you want to achieve. Given these three examples (they are demo apps you can install from the Play Store), which if any has the type of parallax effect you want? Please answer in a comment.
Paralloid Demo
Parallax Scroll Demo
Google IO App
Given an answer, we all will find it easier to help out. If you edit your question to include this information, it will be improved.
The following contains an example application published by the author of Paralloid:
https://github.com/chrisjenx/Paralloid/tree/master/paralloidexample
From the GitHub page under the 'Getting Started' section:
Layout
ScrollView
This is an example, please refer to the paralloidexample App for full
code.
<FrameLayout ..>
<FrameLayout
android:id="#+id/top_content"
android:layout_width="match_parent"
android:layout_height="192dp"/>
<uk.co.chrisjenx.paralloid.views.ParallaxScrollView
android:id="#+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout
android:id="#+id/scroll_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="192dp"/>
</uk.co.chrisjenx.paralloid.views.ParallaxScrollView>
</FrameLayout>
Fragment
Inside your onViewCreated() or onCreateView().
//...
FrameLayout topContent = (FrameLayout) rootView.findViewById(R.id.top_content);
ScrollView scrollView = (ScrollView) rootView.findViewById(R.id.scroll_view);
if (scrollView instanceof Parallaxor) {
((Parallaxor) scrollView).parallaxViewBy(topContent, 0.5f);
}
// TODO: add content to top/scroll content
Thats it!
Have a look at the Parallaxor interface for applicable Parallax
methods.
Hope this helps!
Also, here is a link to Google's 'getting started' page for android.
Also, here is a link to a 'java tutorial for complete beginners'.
As well as link to some documentation about layouts, which 'define the visual structure for a user interface'.
That being said, you would use the layout to define what the interface looks like and use the subsequent example code to define what happens when you interact with it.
P.S. You can see the application in action here
I use the ParallaxScroll library. Very easy to use, good samples and well documented.
Here is how it can be done using ScrollView and it's background image. I've committed the code in github.
You need to extend the ScrollView and Drawable classes.
By default the ScrollView background height will be same as viewport height. To achieve the parallax effect, the background height should be larger and should be based on the ScrollView child height and the background scrolling factor we want to impose.
Background scroll factor of 1 indicates, background height is same as ScrollView child height and hence background will scroll with same offset as the child scrolls.
0.5 indicates, background height is 0.5 times ScrollView child extended height and will scroll 50% slower compared to the child contents. This effectively brings the parallax scrolling effect.
Call following method from ScrollView constructor:
void init() {
// Calculate background drawable size before first draw of scrollview
getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
#Override
public boolean onPreDraw() {
// Remove the listener
getViewTreeObserver().removeOnPreDrawListener(this);
mDrawable = (ParallaxDrawable) getBackground();
if(mDrawable != null && mDrawable instanceof ParallaxDrawable) {
// Get the only child of scrollview
View child = getChildAt(0);
int width = child.getWidth();
// calculate height of background based on child height and scroll factor
int height = (int) (getHeight() + (child.getHeight() - getHeight()) * mScrollFactor);
mDrawable.setSize(width, height);
}
return true;
}
});
}
When ScrollView is scrolled, take into consideration the scroll offset while drawing the background. This basically achieves the parallax effect.
ParallaxScrollView:
protected void onScrollChanged(int x, int y, int oldX, int oldY) {
if(mDrawable != null && mDrawable instanceof ParallaxDrawable) {
// set the scroll offset for the background drawable.
mDrawable.setScrollOffset(x*mScrollFactor, y*mScrollFactor);
}
}
ParallaxDrawable:
#Override
public void draw(Canvas canvas) {
// To move the background up, translate canvas by negative offset
canvas.translate(-mScrollXOffset, -mScrollYOffset);
mDrawable.draw(canvas);
canvas.translate(mScrollXOffset, mScrollYOffset);
}
protected void onBoundsChange(Rect bounds) {
// This sets the size of background drawable.
mDrawable.setBounds(new Rect(bounds.top, bounds.left, bounds.left + mWidth, bounds.top + mHeight));
}
Usage of ParallaxScrollView and ParallaxDrawable:
public class MyActivity extends Activity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.parallax_layout);
final ParallaxScrollView scrollView = (ParallaxScrollView) findViewById(R.id.sv);
ParallaxDrawable drawable = new ParallaxDrawable(getResources().getDrawable(R.drawable.bg));
scrollView.setBackground( drawable, 0.2f );
}
}
parallax_layout.xml:
<manish.com.parallax.ParallaxScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/sv"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="#+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#fff"
android:text="#string/text" />
<View
android:layout_width="match_parent"
android:layout_height="5dp" />
...
</LinearLayout>
</manish.com.parallax.ParallaxScrollView>
The Android API does not support much concrete tools for it as you probably noticed. In API 20 they added elevation which is an attribute for depth. This does not support parallax layouts itself but I would say it's a step by Google to make this kind of work easier. If you want a wild guess on if and when, I would say that parallax utilities could be added before API 25 is released, based on the latest update and the progress in battery efficiency.
For now all you need is to listen for some kind of movement and change the views positions based on a value representing elevation.
Your question made me upgrade my own project and this is how I did it using ViewDragHelper inside a Fragment.
public class MainFragment extends Fragment implements View.OnTouchListener {
private ImageView mDecor, mBamboo, mBackgroundBamboo;
private RelativeLayout mRootLayout;
private ViewDragHelper mDragHelper;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
mRootLayout = (RelativeLayout) inflater.inflate(R.layout.fragment_main, container, false);
mRootLayout.setOnTouchListener(this);
mDecor = (ImageView) mRootLayout.findViewById(R.id.decor);
mBamboo = (ImageView) mRootLayout.findViewById(R.id.bamboo);
mBackgroundBamboo = (ImageView) mRootLayout.findViewById(R.id.backround_bamboo);
mDragHelper = ViewDragHelper.create(mRootLayout, 1.0f, new ViewDragHelper.Callback() {
private final float MAX_LEFT = -0;
private final float MAX_TOP = -20;
private final float MAX_RIGHT = 50;
private final float MAX_BOTTOM = 10;
private final float MULTIPLIER = 0.1f;
private final int DECOR_ELEVATION = 3;
private final int FRONT_BAMBOO_ELEVATION = 6;
private final int BACKGROUND_BAMBOO_ELEVATION = 1;
private float mLeft = 0;
private float mTop = 0;
#Override
public boolean tryCaptureView(View view, int i) {
return true;
}
#Override
public int clampViewPositionVertical(View child, int top, int dy) {
mTop += dy * MULTIPLIER;
mTop = mTop > MAX_BOTTOM ? MAX_BOTTOM : mTop < MAX_TOP ? MAX_TOP : mTop;
mDecor.setTranslationY(mTop * DECOR_ELEVATION);
mBamboo.setTranslationY(mTop * FRONT_BAMBOO_ELEVATION);
mBackgroundBamboo.setTranslationY(mTop * BACKGROUND_BAMBOO_ELEVATION);
return 0;
}
#Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
mLeft += dx * MULTIPLIER;
mLeft = mLeft < MAX_LEFT ? MAX_LEFT : mLeft > MAX_RIGHT ? MAX_RIGHT : mLeft;
mDecor.setTranslationX(mLeft * DECOR_ELEVATION);
mBamboo.setTranslationX(mLeft * FRONT_BAMBOO_ELEVATION);
mBackgroundBamboo.setTranslationX(mLeft * BACKGROUND_BAMBOO_ELEVATION);
return 0;
}
#Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy){
mRootLayout.requestLayout();
}
});
return mRootLayout;
}
#Override
public boolean onTouch(View view, MotionEvent motionEvent) {
mDragHelper.processTouchEvent(motionEvent);
// you can still use this touch listener for buttons etc.
return true;
}
}
Hi You can go with the below-given code for ParallaxView class
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import java.util.ArrayList;
public class ParallaxView extends SurfaceView implements Runnable {
private volatile boolean running;
private Thread gameThread = null;
// For drawing
private Paint paint;
private Canvas canvas;
private SurfaceHolder ourHolder;
// Holds a reference to the Activity
Context context;
// Control the fps
long fps =60;
// Screen resolution
int screenWidth;
int screenHeight;
ParallaxView(Context context, int screenWidth, int screenHeight) {
super(context);
this.context = context;
this.screenWidth = screenWidth;
this.screenHeight = screenHeight;
// Initialize our drawing objects
ourHolder = getHolder();
paint = new Paint();
}
#Override
public void run() {
while (running) {
long startFrameTime = System.currentTimeMillis();
update();
draw();
// Calculate the fps this frame
long timeThisFrame = System.currentTimeMillis() - startFrameTime;
if (timeThisFrame >= 1) {
fps = 1000 / timeThisFrame;
}
}
}
private void update() {
// Update all the background positions
}
private void draw() {
if (ourHolder.getSurface().isValid()) {
//First we lock the area of memory we will be drawing to
canvas = ourHolder.lockCanvas();
//draw a background color
canvas.drawColor(Color.argb(255, 0, 3, 70));
// Draw the background parallax
// Draw the rest of the game
paint.setTextSize(60);
paint.setColor(Color.argb(255, 255, 255, 255));
canvas.drawText("I am a plane", 350, screenHeight / 100 * 5, paint);
paint.setTextSize(220);
canvas.drawText("I'm a train", 50, screenHeight / 100*80, paint);
// Draw the foreground parallax
// Unlock and draw the scene
ourHolder.unlockCanvasAndPost(canvas);
}
}
// Clean up our thread if the game is stopped
public void pause() {
running = false;
try {
gameThread.join();
} catch (InterruptedException e) {
// Error
}
}
// Make a new thread and start it
// Execution moves to our run method
public void resume() {
running = true;
gameThread = new Thread(this);
gameThread.start();
}
}// End of ParallaxView
To know more you can go **
here
**: http://gamecodeschool.com/android/coding-a-parallax-scrolling-background-for-android/
Background:
Google suggests to avoid using nested weighted linearLayouts because of performance.
using nested weighted linearLayout is awful to read, write and maintain.
there is still no good alternative for putting views that are % of the available size. Only solutions are weights and using OpenGL. There isn't even something like the "viewBox" shown on WPF/Silverlight to auto-scale things.
This is why I've decided to create my own layout which you tell for each of its children exactly what should be their weights (and surrounding weights) compared to its size.
It seems I've succeeded , but for some reason I think there are some bugs which I can't track down.
One of the bugs is that textView, even though I give a lot of space for it, it puts the text on the top instead of in the center. imageViews on the other hand work very well. Another bug is that if I use a layout (for example a frameLayout) inside my customized layout, views within it won't be shown (but the layout itself will).
Please help me figure out why it occurs.
How to use: instead of the next usage of linear layout (I use a long XML on purpose, to show how my solution can shorten things):
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" android:orientation="vertical">
<View android:layout_width="wrap_content" android:layout_height="0px"
android:layout_weight="1" />
<LinearLayout android:layout_width="match_parent"
android:layout_height="0px" android:layout_weight="1"
android:orientation="horizontal">
<View android:layout_width="0px" android:layout_height="wrap_content"
android:layout_weight="1" />
<TextView android:layout_width="0px" android:layout_weight="1"
android:layout_height="match_parent" android:text="#string/hello_world"
android:background="#ffff0000" android:gravity="center"
android:textSize="20dp" android:textColor="#ff000000" />
<View android:layout_width="0px" android:layout_height="wrap_content"
android:layout_weight="1" />
</LinearLayout>
<View android:layout_width="wrap_content" android:layout_height="0px"
android:layout_weight="1" />
</LinearLayout>
What I do is simply (the x is where to put the view itself in the weights list):
<com.example.weightedlayouttest.WeightedLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res/com.example.weightedlayouttest"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" tools:context=".MainActivity">
<TextView android:layout_width="0px" android:layout_height="0px"
app:horizontalWeights="1,1x,1" app:verticalWeights="1,1x,1"
android:text="#string/hello_world" android:background="#ffff0000"
android:gravity="center" android:textSize="20dp" android:textColor="#ff000000" />
</com.example.weightedlayouttest.WeightedLayout>
My code of the special layout is:
public class WeightedLayout extends ViewGroup
{
#Override
protected WeightedLayout.LayoutParams generateDefaultLayoutParams()
{
return new WeightedLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);
}
#Override
public WeightedLayout.LayoutParams generateLayoutParams(final AttributeSet attrs)
{
return new WeightedLayout.LayoutParams(getContext(),attrs);
}
#Override
protected ViewGroup.LayoutParams generateLayoutParams(final android.view.ViewGroup.LayoutParams p)
{
return new WeightedLayout.LayoutParams(p.width,p.height);
}
#Override
protected boolean checkLayoutParams(final android.view.ViewGroup.LayoutParams p)
{
final boolean isCorrectInstance=p instanceof WeightedLayout.LayoutParams;
return isCorrectInstance;
}
public WeightedLayout(final Context context)
{
super(context);
}
public WeightedLayout(final Context context,final AttributeSet attrs)
{
super(context,attrs);
}
public WeightedLayout(final Context context,final AttributeSet attrs,final int defStyle)
{
super(context,attrs,defStyle);
}
#Override
protected void onLayout(final boolean changed,final int l,final int t,final int r,final int b)
{
for(int i=0;i<this.getChildCount();++i)
{
final View v=getChildAt(i);
final WeightedLayout.LayoutParams layoutParams=(WeightedLayout.LayoutParams)v.getLayoutParams();
//
final int availableWidth=r-l;
final int totalHorizontalWeights=layoutParams.getLeftHorizontalWeight()+layoutParams.getViewHorizontalWeight()+layoutParams.getRightHorizontalWeight();
final int left=l+layoutParams.getLeftHorizontalWeight()*availableWidth/totalHorizontalWeights;
final int right=r-layoutParams.getRightHorizontalWeight()*availableWidth/totalHorizontalWeights;
//
final int availableHeight=b-t;
final int totalVerticalWeights=layoutParams.getTopVerticalWeight()+layoutParams.getViewVerticalWeight()+layoutParams.getBottomVerticalWeight();
final int top=t+layoutParams.getTopVerticalWeight()*availableHeight/totalVerticalWeights;
final int bottom=b-layoutParams.getBottomVerticalWeight()*availableHeight/totalVerticalWeights;
//
v.layout(left+getPaddingLeft(),top+getPaddingTop(),right+getPaddingRight(),bottom+getPaddingBottom());
}
}
// ///////////////
// LayoutParams //
// ///////////////
public static class LayoutParams extends ViewGroup.LayoutParams
{
int _leftHorizontalWeight =0,_rightHorizontalWeight=0,_viewHorizontalWeight=0;
int _topVerticalWeight =0,_bottomVerticalWeight=0,_viewVerticalWeight=0;
public LayoutParams(final Context context,final AttributeSet attrs)
{
super(context,attrs);
final TypedArray arr=context.obtainStyledAttributes(attrs,R.styleable.WeightedLayout_LayoutParams);
{
final String horizontalWeights=arr.getString(R.styleable.WeightedLayout_LayoutParams_horizontalWeights);
//
// handle horizontal weight:
//
final String[] words=horizontalWeights.split(",");
boolean foundViewHorizontalWeight=false;
int weight;
for(final String word : words)
{
final int viewWeightIndex=word.lastIndexOf('x');
if(viewWeightIndex>=0)
{
if(foundViewHorizontalWeight)
throw new IllegalArgumentException("found more than one weights for the current view");
weight=Integer.parseInt(word.substring(0,viewWeightIndex));
setViewHorizontalWeight(weight);
foundViewHorizontalWeight=true;
}
else
{
weight=Integer.parseInt(word);
if(weight<0)
throw new IllegalArgumentException("found negative weight:"+weight);
if(foundViewHorizontalWeight)
_rightHorizontalWeight+=weight;
else _leftHorizontalWeight+=weight;
}
}
if(!foundViewHorizontalWeight)
throw new IllegalArgumentException("couldn't find any weight for the current view. mark it with 'x' next to the weight value");
}
//
// handle vertical weight:
//
{
final String verticalWeights=arr.getString(R.styleable.WeightedLayout_LayoutParams_verticalWeights);
final String[] words=verticalWeights.split(",");
boolean foundViewVerticalWeight=false;
int weight;
for(final String word : words)
{
final int viewWeightIndex=word.lastIndexOf('x');
if(viewWeightIndex>=0)
{
if(foundViewVerticalWeight)
throw new IllegalArgumentException("found more than one weights for the current view");
weight=Integer.parseInt(word.substring(0,viewWeightIndex));
setViewVerticalWeight(weight);
foundViewVerticalWeight=true;
}
else
{
weight=Integer.parseInt(word);
if(weight<0)
throw new IllegalArgumentException("found negative weight:"+weight);
if(foundViewVerticalWeight)
_bottomVerticalWeight+=weight;
else _topVerticalWeight+=weight;
}
}
if(!foundViewVerticalWeight)
throw new IllegalArgumentException("couldn't find any weight for the current view. mark it with 'x' next to the weight value");
}
//
arr.recycle();
}
public LayoutParams(final int width,final int height)
{
super(width,height);
}
public LayoutParams(final ViewGroup.LayoutParams source)
{
super(source);
}
public int getLeftHorizontalWeight()
{
return _leftHorizontalWeight;
}
public void setLeftHorizontalWeight(final int leftHorizontalWeight)
{
_leftHorizontalWeight=leftHorizontalWeight;
}
public int getRightHorizontalWeight()
{
return _rightHorizontalWeight;
}
public void setRightHorizontalWeight(final int rightHorizontalWeight)
{
if(rightHorizontalWeight<0)
throw new IllegalArgumentException("negative weight :"+rightHorizontalWeight);
_rightHorizontalWeight=rightHorizontalWeight;
}
public int getViewHorizontalWeight()
{
return _viewHorizontalWeight;
}
public void setViewHorizontalWeight(final int viewHorizontalWeight)
{
if(viewHorizontalWeight<0)
throw new IllegalArgumentException("negative weight:"+viewHorizontalWeight);
_viewHorizontalWeight=viewHorizontalWeight;
}
public int getTopVerticalWeight()
{
return _topVerticalWeight;
}
public void setTopVerticalWeight(final int topVerticalWeight)
{
if(topVerticalWeight<0)
throw new IllegalArgumentException("negative weight :"+topVerticalWeight);
_topVerticalWeight=topVerticalWeight;
}
public int getBottomVerticalWeight()
{
return _bottomVerticalWeight;
}
public void setBottomVerticalWeight(final int bottomVerticalWeight)
{
if(bottomVerticalWeight<0)
throw new IllegalArgumentException("negative weight :"+bottomVerticalWeight);
_bottomVerticalWeight=bottomVerticalWeight;
}
public int getViewVerticalWeight()
{
return _viewVerticalWeight;
}
public void setViewVerticalWeight(final int viewVerticalWeight)
{
if(viewVerticalWeight<0)
throw new IllegalArgumentException("negative weight :"+viewVerticalWeight);
_viewVerticalWeight=viewVerticalWeight;
}
}
}
I accepted your challenge and attempted to create the layout you describe in response to my comment. You are right. It is surprisingly difficult to accomplish. Besides that, I do like shooting house flies. So I jumped on board and came up with this solution.
Extend the existing layout classes rather than creating your own from scratch. I went with RelativeLayout to start with but the same approach can be used by all of them. This gives you the ability to use the default behavior for that layout on child views that you don't want to manipulate.
I added four attributes to the layout called top, left, width and height. My intention was to mimic HTML by allowing values such as "10%", "100px", "100dp" etc.. At this time the only value accepted is an integer representing the % of parent. "20" = 20% of the layout.
For better performance I allow the super.onLayout() to execute through all of it's iterations and only manipulate the views with the custom attributes on it's last pass. Since these views will be positioned and scaled independently of the siblings we can move them after everything else has settled.
Here is atts.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="HtmlStyleLayout">
<attr name="top" format="integer"/>
<attr name="left" format="integer"/>
<attr name="height" format="integer"/>
<attr name="width" format="integer"/>
</declare-styleable>
</resources>
Here is my layout class.
package com.example.helpso;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.RelativeLayout;
public class HtmlStyleLayout extends RelativeLayout{
private int pass =0;
#Override
protected HtmlStyleLayout.LayoutParams generateDefaultLayoutParams()
{
return new HtmlStyleLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT,RelativeLayout.LayoutParams.WRAP_CONTENT);
}
#Override
public HtmlStyleLayout.LayoutParams generateLayoutParams(final AttributeSet attrs)
{
return new HtmlStyleLayout.LayoutParams(getContext(),attrs);
}
#Override
protected RelativeLayout.LayoutParams generateLayoutParams(final android.view.ViewGroup.LayoutParams p)
{
return new HtmlStyleLayout.LayoutParams(p.width,p.height);
}
#Override
protected boolean checkLayoutParams(final android.view.ViewGroup.LayoutParams p)
{
final boolean isCorrectInstance=p instanceof HtmlStyleLayout.LayoutParams;
return isCorrectInstance;
}
public HtmlStyleLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setScaleType(View v){
try{
((ImageView) v).setScaleType (ImageView.ScaleType.FIT_XY);
}catch (Exception e){
// The view is not an ImageView
}
}
#Override
protected void onLayout(final boolean changed,final int l,final int t,final int r,final int b)
{
super.onLayout(changed, l, t, r, b); //Let the parent layout do it's thing
pass++; // After the last pass of
final int childCount = this.getChildCount(); // the parent layout
if(true){ // we do our thing
for(int i=0;i<childCount;++i)
{
final View v=getChildAt(i);
final HtmlStyleLayout.LayoutParams params = (HtmlStyleLayout.LayoutParams)v.getLayoutParams();
int newTop = v.getTop(); // set the default value
int newLeft = v.getLeft(); // of these to the value
int newBottom = v.getBottom(); // set by super.onLayout()
int newRight= v.getRight();
boolean viewChanged = false;
if(params.getTop() >= 0){
newTop = ( (int) ((b-t) * (params.getTop() * .01)) );
viewChanged = true;
}
if(params.getLeft() >= 0){
newLeft = ( (int) ((r-l) * (params.getLeft() * .01)) );
viewChanged = true;
}
if(params.getHeight() > 0){
newBottom = ( (int) ((int) newTop + ((b-t) * (params.getHeight() * .01))) );
setScaleType(v); // set the scale type to fitxy
viewChanged = true;
}else{
newBottom = (newTop + (v.getBottom() - v.getTop()));
Log.i("heightElse","v.getBottom()=" +
Integer.toString(v.getBottom())
+ " v.getTop=" +
Integer.toString(v.getTop()));
}
if(params.getWidth() > 0){
newRight = ( (int) ((int) newLeft + ((r-l) * (params.getWidth() * .01))) );
setScaleType(v);
viewChanged = true;
}else{
newRight = (newLeft + (v.getRight() - v.getLeft()));
}
// only call layout() if we changed something
if(viewChanged)
Log.i("SizeLocation",
Integer.toString(i) + ": "
+ Integer.toString(newLeft) + ", "
+ Integer.toString(newTop) + ", "
+ Integer.toString(newRight) + ", "
+ Integer.toString(newBottom));
v.layout(newLeft, newTop, newRight, newBottom);
}
pass = 0; // reset the parent pass counter
}
}
public class LayoutParams extends RelativeLayout.LayoutParams
{
private int top, left, width, height;
public LayoutParams(final Context context, final AttributeSet atts) {
super(context, atts);
TypedArray a = context.obtainStyledAttributes(atts, R.styleable.HtmlStyleLayout);
top = a.getInt(R.styleable.HtmlStyleLayout_top , -1);
left = a.getInt(R.styleable.HtmlStyleLayout_left, -1);
width = a.getInt(R.styleable.HtmlStyleLayout_width, -1);
height = a.getInt(R.styleable.HtmlStyleLayout_height, -1);
a.recycle();
}
public LayoutParams(int w, int h) {
super(w,h);
Log.d("lp","2");
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
Log.d("lp","3");
}
public LayoutParams(ViewGroup.MarginLayoutParams source) {
super(source);
Log.d("lp","4");
}
public int getTop(){
return top;
}
public int getLeft(){
return left;
}
public int getWidth(){
return width;
}
public int getHeight(){
return height;
}
}
}
Here is an example activity xml
<com.example.helpso.HtmlStyleLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:html="http://schemas.android.com/apk/res/com.example.helpso"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<ImageView
android:id="#+id/imageView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:scaleType="fitXY"
android:src="#drawable/bg" />
<ImageView
android:id="#+id/imageView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="#drawable/overlay"
html:height="10"
html:left="13"
html:top="18"
html:width="23" />
</com.example.helpso.HtmlStyleLayout>
Here are the images I used for testing.
If you do not set a value for a particular attribute it's default will be used. So if you set width but not height the image will scale in width and wrap_content for height.
Zipped project folder.
apk
I found the source of the bug. The problem is that I was using the layout's child count as in indicator of how many calls to onLayout it will make. This doesn't seem to hold true in older versions of android. I noticed in 2.1 onLayout is only called once. So I changed
if(pass == childCount){
to
if(true){
and it started working as expected.
I still thinks it's beneficial to adjust the layout only after the super is done. Just need to find a better way to know when that is.
EDIT
I didn't realize that your intention was to patch together images with pixel by pixel precision. I achieved the precision you are looking for by using double float precision variables instead of integers. However, you will not be able accomplish this while allowing your images to scale. When an images is scaled up pixels are added at some interval between the existing pixels. The color of the new pixels are some weighted average of the surrounding pixels. When you scale the images independently of each other they don't share any information. The result is that you will always have some artifact at the seam. Add to that the result of rounding since you can't have a partial pixel and you will always have a +/-1 pixel tolerance.
To verify this you can attempt the same task in your premium photo editing software. I use PhotoShop. Using the same images as in my apk, I placed them in seperate files. I scaled them both by 168% vertically and 127% horizontally. I then placed them in a file together and attempted to align them. The result was exactly the same as is seen in my apk.
To demonstrate the accuracy of the layout, I added a second activity to my apk. On this activity I did not scale the background image. Everything else is exactly the same. The result is seamless.
I also added a button to show/hide the overlay image and one to switch between the activities.
I updated both the apk and the zipped project folder on my google drive. You can get them by the links above.
After trying your code, I just find the reason of the problems you mentioned, and it is because in your customed layout, you only layout the child properly, however you forgot to measure your child properly, which will directly affect the drawing hierarchy, so simply add the below code, and it works for me.
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec)-this.getPaddingRight()-this.getPaddingRight();
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec)-this.getPaddingTop()-this.getPaddingBottom();
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if(heightMode == MeasureSpec.UNSPECIFIED || widthMode == MeasureSpec.UNSPECIFIED)
throw new IllegalArgumentException("the layout must have a exact size");
for (int i = 0; i < this.getChildCount(); ++i) {
View child = this.getChildAt(i);
LayoutParams lp = (LayoutParams)child.getLayoutParams();
int width = lp._viewHorizontalWeight * widthSize/(lp._leftHorizontalWeight+lp._rightHorizontalWeight+lp._viewHorizontalWeight);
int height = lp._viewVerticalWeight * heightSize/(lp._topVerticalWeight+lp._bottomVerticalWeight+lp._viewVerticalWeight);
child.measure(width | MeasureSpec.EXACTLY, height | MeasureSpec.EXACTLY);
}
this.setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));
}
Now there is a nicer solution than the custom layout I've made:
PercentRelativeLayout
Tutorial can be found here and a repo can be found here.
Example code:
<android.support.percent.PercentRelativeLayout
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="match_parent"/>
<ImageView
app:layout_widthPercent="50%"
app:layout_heightPercent="50%"
app:layout_marginTopPercent="25%"
app:layout_marginLeftPercent="25%"/>
</android.support.percent.PercentFrameLayout/>
or:
<android.support.percent.PercentFrameLayout
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="match_parent"/>
<ImageView
app:layout_widthPercent="50%"
app:layout_heightPercent="50%"
app:layout_marginTopPercent="25%"
app:layout_marginLeftPercent="25%"/>
</android.support.percent.PercentFrameLayout/>
I wonder though if it can handle the issues I've shown here.
I propose to use following optimizations:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" android:gravity="center">
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content" android:text="#string/hello_world"
android:background="#ffff0000" android:gravity="center"
android:textSize="20dp" android:textColor="#ff000000" />
</FrameLayout>
or use http://developer.android.com/reference/android/widget/LinearLayout.html#attr_android:weightSum
or use TableLayout with layout_weight for rows and columns
or use GridLayout.
I am animating three views that are stacked on top of each other. When I tap one that is not the front view, one or two views will slide up or down to uncover the tapped view, bring the tapped view to front, and then return everything to their original position. Most of these work fine. Only when I bring a view to front that I just animated away I get a noticeable flicker.
I have read at least a hundred posts but none contains the solution.
I am posting this to consolidate every suggested solution in one place and to hopefully find a solution.
I know that the animation does not animate the view itself, but just an image. The view stays at its original position. It is definitely related to that. It only happens when bringing a view to front that just moved.
Moving the view to the animation end position before starting the animation or after the animation is finished does not help one bit.
It also is not related to the AnimationListener.onAnimationEnd bug, since I derived my own views and intercept onAnimationEnd there.
I am using Animation.setFillAfter and Animation.setFillEnabled to keep the final image at the animation end location.
I tried using Animation.setZAdjustment but that one only works for entire screens, not views within a screen.
From what I have learned I suspect that the problem is bringToFront() itself, which does a removeChild()/addChild() on the parent view. Maybe the removeChild causes the redraw showing the view without the removed child briefly.
So my questions: Does anyone see anything I missed that could fix this?
Does Android maybe have a command to temporarily stop drawing and resume drawing later. Something like a setUpdateScreen(false) / setUpdateScreen(true) pair?
That would allow me to skip the flicker stage.
Minimal code to demo the effect follows. Tap white to see red move up and back down behind white without flicker (white comes to front but does not move). Then tap red to see red move back up from behind white and the flicker when it is brought to front just before it slides back down over white. Weird thing is that the same thing does not always happen when using blue instead of red.
MainActivity.java
package com.example.testapp;
import com.example.testapp.ImagePanel.AnimationEndListener;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.animation.TranslateAnimation;
public class MainActivity extends Activity
{
private static final int ANIMATION_TIME = 1000;
private ImagePanel mRed;
private ImagePanel mWhite;
private ImagePanel mBlue;
private int mFrontPanelId;
private void animate(final ImagePanel panel, final int yFrom, final int yTo,
final AnimationEndListener animationListener)
{
final TranslateAnimation anim = new TranslateAnimation(0, 0, 0, 0, 0, yFrom, 0, yTo);
anim.setDuration(ANIMATION_TIME);
anim.setFillAfter(true);
anim.setFillEnabled(true);
if (animationListener != null)
{
panel.setAnimListener(animationListener);
}
panel.startAnimation(anim);
}
public void onClick(final View v)
{
final int panelId = v.getId();
if (mFrontPanelId == panelId)
{
return;
}
final ImagePanel panel = (ImagePanel) v;
final int yTop = mWhite.getTop() - mRed.getBottom();
final int yBot = mWhite.getBottom() - mBlue.getTop();
final boolean moveRed = panelId == R.id.red || mFrontPanelId == R.id.red;
final boolean moveBlue = panelId == R.id.blue || mFrontPanelId == R.id.blue;
animate(mBlue, 0, moveBlue ? yBot : 0, null);
animate(mRed, 0, moveRed ? yTop : 0, new AnimationEndListener()
{
public void onBegin()
{
}
public void onEnd()
{
// make sure middle panel always stays visible
if (moveRed && moveBlue)
{
mWhite.bringToFront();
}
panel.bringToFront();
animate(mBlue, moveBlue ? yBot : 0, 0, null);
animate(mRed, moveRed ? yTop : 0, 0, new AnimationEndListener()
{
public void onBegin()
{
}
public void onEnd()
{
}
});
mFrontPanelId = panelId;
}
});
}
#Override
public void onCreate(final Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mRed = (ImagePanel) findViewById(R.id.red);
mWhite = (ImagePanel) findViewById(R.id.white);
mBlue = (ImagePanel) findViewById(R.id.blue);
mFrontPanelId = R.id.red;
}
}
ImagePanel.java
package com.example.testapp;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.ImageView;
public class ImagePanel extends ImageView
{
public interface AnimationEndListener
{
public void onBegin();
public void onEnd();
}
private AnimationEndListener mAnim = null;
public ImagePanel(final Context context)
{
super(context);
}
public ImagePanel(final Context context, final AttributeSet attrs)
{
super(context, attrs);
}
public ImagePanel(final Context context, final AttributeSet attrs, final int defStyle)
{
super(context, attrs, defStyle);
}
#Override
protected void onAnimationEnd()
{
super.onAnimationEnd();
clearAnimation();
if (mAnim != null)
{
final AnimationEndListener anim = mAnim;
mAnim = null;
anim.onEnd();
}
}
#Override
protected void onAnimationStart()
{
super.onAnimationStart();
if (mAnim != null)
{
mAnim.onBegin();
}
}
public void setAnimListener(final AnimationEndListener anim)
{
mAnim = anim;
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/main"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<com.example.testapp.ImagePanel
android:id="#+id/blue"
android:layout_width="300dp"
android:layout_height="300dp"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:background="#000080"
android:src="#drawable/testpattern"
android:onClick="onClick" />
<com.example.testapp.ImagePanel
android:id="#+id/white"
android:layout_width="300dp"
android:layout_height="300dp"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:background="#808080"
android:src="#drawable/testpattern"
android:onClick="onClick" />
<com.example.testapp.ImagePanel
android:id="#+id/red"
android:layout_width="300dp"
android:layout_height="300dp"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:adjustViewBounds="true"
android:background="#800000"
android:src="#drawable/testpattern"
android:onClick="onClick" />
</RelativeLayout>
testpattern.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval" >
<gradient
android:startColor="#00000000"
android:endColor="#ffffffff" />
</shape>
Do you need all the images be visible at any time? you can set their visibility as invisible so they will not disturb you. you make them visible again once you need them.
Try calling animation.cancel() in onAnimationEnd. I remember a while back I had a similar issue with animation flickering after executing code in onAnimationEndand that did the trick.
I was observing the same problems when calling bringToFront() during an animation.
I could solve my problem by using setChildrenDrawingOrderEnabled(boolean enabled) and getChildDrawingOrder(int childCount, int i) on the ViewGroup that contained the children I was animating.
I made a custom component that extends View and overrides its onMeasure(), the content of this component is some text, then I add it to a RelativeLayout, but this text can't display, if I comment onMeasure() that been overridden the text shows. What's the reason?
Here is the code:
public class CustomView extends View {
private String text;
private int viewWidth;
private int viewHeight;
private Paint paint;
private FontMetrics fontMetrics;
public CustomView(Context context) {
this(context, null);
}
public CustomView(Context context, String text) {
this(context, text, 0);
this.text = text;
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
updateViewBounds();
}
public CustomView(Context context, String text, int defStyle) {
super(context);
}
private void updateViewBounds(){
viewWidth = (int) paint.measureText(this.text);
fontMetrics = paint.getFontMetrics();
viewHeight = (int)(fontMetrics.descent - fontMetrics.ascent);
}
private String getText() {
return this.text;
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(viewWidth, viewHeight);
//setMeasuredDimension(560, 100);even though give a ensured size, it can't //anyway.
}
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
paint.setColor(Color.WHITE);
paint.setTextSize(30);
canvas.drawText(text, 0, 200, paint);
Log.e("content", ""+this.getText());
}
public boolean onTouchEvent (MotionEvent event){
Log.e("Touch", ""+this.getText());
return false;
}
}
Here is the Activity:
public class CustomViewActivity extends Activity {
/** Called when the activity is first created. */
private RelativeLayout contentLayout;
private CustomView view1;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
contentLayout = (RelativeLayout)findViewById(R.id.contentLayout);
view1 = new CustomView(this, "You drive me crazy!!!");
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
view1.setLayoutParams(layoutParams);
contentLayout.addView(view1);
}
}
this is the XML file:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/contentLayout"
android:layout_width="1024px"
android:layout_height="560px"
android:orientation="vertical" >
<Button
android:id="#+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_marginLeft="126dp"
android:text="Button" />
</RelativeLayout>
You can absolutely set the MeasureSpec to a different size, however, the arguments for onMeasure are misleading. A MeasureSpec is a specially translated int that has to be specifically created by using both a pixel measure and a flag. The correct way to set a specific size it indicated below...
final int desiredHSpec = MeasureSpec.makeMeasureSpec(pixelHeight, MeasureSpec.MODE_CONSTANT);
final int desiredWSpec = MeasureSpec.makeMeasureSpec(pixelWidth, MeasureSpec.MODE_CONSTANT);
setMeasuredDimension(desiredWSpec, desiredHSpec);
The MODE_CONSTANTS must have a value of one of the following:
* AT_MOST - meaning that it is dynamic, but will be clipped if the contents are too large
* EXACTLY - meaning it will be that size no matter how large or small the contents are
* UNSPECIFIED - meaning that it will make whatever decision it makes according to the parameters of the parents, children, device size, etc...
If you do not specify one of these constants, then the Android Layout rendering engine has no idea what to do, and simply hides the object. It must be understood, that as an open platform for so many devices, Google decided to make the layout engine "dynamic and intelligent" to support as many apps as possible on as many platforms as possible. This simply requires the developer to let the device know exactly what it needs.
Note: It sounds like you want EXACTLY, but think carefully about your choice and how many devices you will be supporting. :)