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'
In my application I use a bottom sheet (from the support library) which works great. Now I would like to animate a layout change while the sheet is dragged up. For this I have created a subclass of BottomSheetCallback (this is normaly an inner class of a Fragment so not all objects used in this calss are initialized here):
public class MyBehavior extends BottomSheetBehavior.BottomSheetCallback {
Transition transition;
float lastOffset = 0;
Scene scene;
public PlayerBehavior() {
TransitionInflater inflater = TransitionInflater.from(getContext());
transition = inflater.inflateTransition(R.transition.player);
//transition.setDuration(300);
scene = fullLayout;
transition.setInterpolator(new Interpolator() {
#Override
public float getInterpolation(float v) {
return lastOffset;
}
});
}
#Override
public void onStateChanged(#NonNull View bottomSheet, int newState) {
if(newState == BottomSheetBehavior.STATE_DRAGGING) {
TransitionManager.go(scene, transition);
}
}
#Override
public void onSlide(View bottomSheet, final float slideOffset) {
scene = (slideOffset > lastOffset) ? smallLayout : fullLayout;
lastOffset = slideOffset;
}
}
As you can see I also created two Scene from different layout files and a custom Transition to animate between the scenes with the TransitionManager. My problem is that the Transition should be based on the slideOffset parameter (in range of 0-1) but the TransitionManager uses the Animation class in the background which is normally time based in Android.
I tried to create the custom Intapolator but this does not work properly. So how can I create a Transition which is based on an external variable and not on time?
Based on your description, I think you are trying to achieve something like google maps bottom sheet behaviour. The layout changes as the bottomsheet is dragged up.
If that is what you are trying to achieve then you don't need to enforce custom animations, as the bottomsheetdialog itself has those animation behaviour when incorporated inside a parent Coordinator Layout.
Here is a sample code of how I'm implementing the same behaviour. It also makes the FloatingActionButton invisible when the bottomsheet is dragged up to full screen size :
Create a bottomsheetdialog that you want to use inside your main layout
public class CustomBottomDialog extends BottomSheetDialogFragment {
String mSomeName;
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// if some arguments are passed from the calling activity
mSomeName = getArguments().getString("some_name");
}
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View bottomSheet = inflater.inflate(R.layout.bottomsheet_layout, container, false);
// initialise your bottomsheet_layout items here
TextView tvName = bottomSheet.findViewById(R.id.display_name);
tvName.setText(mSomeName);
tvName.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
// do something here
((MainActivity)getActivity()).doSomething();
}
});
return bottomSheet;
}
}
bottomsheet_layout:
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.FloatingActionButton
android:id="#+id/nav"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="#drawable/navigation_tilt_grey"
app:backgroundTint="#color/colorAccent"
app:elevation="3dp"
app:fabSize="normal"
android:layout_marginEnd="#dimen/activity_horizontal_margin"
app:layout_anchor="#+id/live_dash"
app:layout_anchorGravity="top|right" />
<!--BottomSheet-->
<android.support.v4.widget.NestedScrollView
android:id="#+id/live_dash"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#F3F3F3"
android:clipToPadding="true"
app:layout_behavior="android.support.design.widget.BottomSheetBe
havior"
tools:layout_editor_absoluteY="150dp">
<!--Include your items here, the height of all items combined
will take the main screen layout size with animation-->
</android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>
Calling this BottomSheet from your activity:
public void notifyBottomSheet(String somename){
BottomSheetDialogFragment customDialogFragment = new CustomBottomDialog();
Bundle args = new Bundle();
args.putString("some_name", somename);
customDialogFragment.setArguments(args);
customDialogFragment.show(getSupportFragmentManager(), customDialogFragment.getTag());
customDialogFragment.setCancelable(false); // if you don't wish to hide
}
Hope this solves what you are trying to achieve.
To easily slide something off the bottom of the screen, you can use code such as:
final int activityHeight = findViewById(android.R.id.content).getHeight();
cardContainer.animate().yBy(activityHeight - cardContainer.getY()).setDuration(SLIDE_OUT_DURATION);
where cardContainer is the view you are trying to slide off the screen.
See this blog post for the complete example. Note that you can also use translationY instead of yBy. Another, more generic way of doing it is with this code:
public static ViewPropertyAnimator slideOutToBottom(Context ctx, View view) {
final int screenHeight = ctx.getResources().getDisplayMetrics().heightPixels;
int[] coords = new int[2];
view.getLocationOnScreen(coords);
return view.animate().translationY(screenHeight - coords[Y_INDEX]).setDuration(SLIDE_OUT_DURATION);
}
public static ViewPropertyAnimator slideInFromBottom(Context ctx, View view) {
final int screenHeight = ctx.getResources().getDisplayMetrics().heightPixels;
int[] coords = new int[2];
view.getLocationOnScreen(coords);
view.setTranslationY(screenHeight - coords[Y_INDEX]);
return view.animate().translationY(0).setDuration(SLIDE_IN_DURATION).setInterpolator(new OvershootInterpolator(1f));
}
## Translation Animation ##
<?xml version="1.0" encoding="utf-8"?>
<set
xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="#android:anim/accelerate_decelerate_interpolator"
android:fillAfter="true"
>
<translate
android:fromYDelta="100%p"
android:toYDelta="-30%p"
android:duration="900" />
</set>
## Main Activity ##
#Override
protected void onResume() {
super.onResume();
Animation am= AnimationUtils.loadAnimation(this,R.anim.fadeout);
tv5.startAnimation(am);
Animation myanim= AnimationUtils.loadAnimation(this,R.anim.translate);
tv1.startAnimation(myanim);
myanim.setStartOffset(500);
Animation animation= AnimationUtils.loadAnimation(this,R.anim.translate);
animation.setStartOffset(1000);
tv2.startAnimation(animation);
Animation an= AnimationUtils.loadAnimation(this,R.anim.translate);
an.setStartOffset(1500);
tv3.startAnimation(an);
Animation ab= AnimationUtils.loadAnimation(this,R.anim.translate);
ab.setStartOffset(2000);
tv4.startAnimation(ab);
Animation ac= AnimationUtils.loadAnimation(this,R.anim.fadein);
ac.setStartOffset(2500);
btn1.startAnimation(ac);
}
I'm not sure if that is what you want but maybe instead of using transition, you can use the function animate() since with this function, you can change all things about your animation (time, visibility etc.).
I made a custom View that is used to show feedback in the UI (usually in response to an action being taken). When FeedbackView.showText is called, it will animate the View in for 2 seconds, and then animate it out. This is done using translationY.
If I apply a negative margin to it that is greater than its height the first time FeedbackView.showText is called it doesn't animate in correctly; it just appears (or in some cases doesn't display at all). Subsequent calls to FeedbackView.showText cause the correct animation.
In activity_main.xml below, the FeedbackView has a margin top of -36dp, which is greater than its height (when non-negated). If the margin top is changed to -35dp it animates correctly even the first time FeedbackView.showText is called.
Does anyone know why something like this would happen?
Romain Guy has said that it is OK to use negative margins on LinearLayout and RelativeLayout. My only guess is that they are not OK with FrameLayouts.
FeedbackView.java
public class FeedbackView extends FrameLayout {
public static final int DEFAULT_SHOW_DURATION = 2000;
private AtomicBoolean showing = new AtomicBoolean(false);
private AtomicBoolean animating = new AtomicBoolean(false);
private float heightOffset;
private Runnable animateOutRunnable = new Runnable() {
#Override
public void run() {
animateContainerOut();
}
};
public FeedbackView(Context context) {
super(context);
init();
}
public FeedbackView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public FeedbackView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
public FeedbackView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
private void init() {
setAlpha(0);
}
public boolean isShowing() {
return showing.get();
}
public void showText(Context context, String text) {
removeCallbacks(animateOutRunnable);
heightOffset = getMeasuredHeight();
removeAllViews();
final TextView tv = new TextView(context);
tv.setGravity(Gravity.CENTER);
tv.setTextColor(Color.WHITE);
tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
tv.setText(text);
addView(tv);
if(!showing.getAndSet(true)) {
animateContainerIn();
}
else {
tv.setTranslationY(-getHeight());
tv.animate().translationY(0).start();
}
postDelayed(animateOutRunnable, DEFAULT_SHOW_DURATION);
}
private void animateContainerIn() {
if(animating.getAndSet(true)) {
animate().cancel();
}
ViewPropertyAnimator animator = animate();
long startDelay = animator.getDuration() / 2;
animate()
.alpha(1)
.setStartDelay(startDelay)
.start();
animate()
.translationY(heightOffset)
.setStartDelay(0)
.withEndAction(new Runnable() {
#Override
public void run() {
animating.set(false);
showing.set(true);
}
})
.start();
}
private void animateContainerOut() {
showing.set(false);
if(animating.getAndSet(true)) {
animate().cancel();
}
ViewPropertyAnimator animator = animate();
long duration = animator.getDuration();
animate()
.alpha(0)
.setDuration(duration / 2)
.start();
animate()
.translationY(-heightOffset)
.setDuration(duration)
.withEndAction(new Runnable() {
#Override
public void run() {
animating.set(false);
}
})
.start();
}
}
MainActivity.java
public class MainActivity extends Activity {
private FeedbackView feedbackView;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
feedbackView = (FeedbackView) findViewById(R.id.feedback);
findViewById(R.id.show_feedback).setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
feedbackView.showText(MainActivity.this, "Feedback");
}
});
}
}
activity_main.xml
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="70dp"
android:background="#000"
android:clickable="true"/>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="90dp"/>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
android:background="#e9e9e9"/>
<negative.margin.FeedbackView
android:id="#+id/feedback"
android:layout_width="match_parent"
android:layout_height="35dp"
android:layout_marginTop="-36dp"
android:background="#20ACE0"/>
</FrameLayout>
<Button
android:id="#+id/show_feedback"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center"
android:text="Show Feedback"/>
</LinearLayout>
My guess would be that the negative margin is not the direct cause of your animation failing.
You would probably achieve the same (undesired) effect - animation not being performed - if you set for example: layout_marginLeft to the value equal to the Activity width (so a positive value).
The problem is that your View is completely "outside" of the visible area therefore when your Activity is being created, the View is not being rendered right away. And running an animation on a View that has not been rendered yet, will not be performed.
More information (for example) here.
What you can do to fix it is:
Rebuild your layout in the way that your View is within the rendered area (so basically is within the visible area), but its visibility is set to View.INVISIBLE. At the start of the animation (use an AnimationListener or AnimatorListener or something ;) ) set its visibility to View.VISIBLE.
Rebuild your animation so it does not use ViewPropertyAnimator (the animate() method call), but an Animation Object. And start it on a different View (on one that you are sure has already been rendered) - for example on the View's ViewParent (which you can get with getParent()).
You can try (my guts tell me that should work, but you would need to test it) to set your layouts clipChildren and clipToPadding to false, forcing your views to be rendered even when outside of the visibile area. If you try that solution (and I think you should, because you won't have to change that much - just add android:clipChildren="false",android:clipToPadding="false" to all of your layouts in this Activity) please tell me if it worked.
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/
I have problems with the translucent actionbar/navbar in the new Android KitKat (4.4) and the windowSoftInputMode="adjustResize".
Normaly, changing the InputMode to adjustResize, the app should resize itself when keyboard is shown, but here it won't! If I delete the lines for the transparent effect, the resize is working.
So if the keyboard is visible, my ListView is under it and I can't access the last few items (only by hiding the keyboard manually).
AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="XYZ"
android:versionCode="23"
android:versionName="0.1" >
<uses-sdk
android:minSdkVersion="9"
android:targetSdkVersion="19" />
<application
android:allowBackup="true"
android:icon="#drawable/ic_launcher"
android:label="#string/app_name"
android:theme="#style/Theme.XYZStyle" >
<activity
android:name="XYZ"
android:label="#string/app_name"
android:windowSoftInputMode="adjustResize" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
values-v19/styles.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.XYZStyle" parent="#style/Theme.AppCompat.Light">
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowTranslucentNavigation">true</item>
</style>
</resources>
fragment.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="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<ListView
android:id="#+id/listView_contacts"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:divider="#null"
android:dividerHeight="0dp"
android:drawSelectorOnTop="true"
android:fastScrollAlwaysVisible="true"
android:fastScrollEnabled="true"
android:paddingBottom="#dimen/navigationbar__height" >
</ListView>
</RelativeLayout>
Any ideas for fixing this?
You are missing the following property:
android:fitsSystemWindows="true"
in the root RelativeLayout of the fragment .xml layout.
Update:
Last year there was an interesting talk by Chris Bane that explains in good detail how this works:
https://www.youtube.com/watch?v=_mGDMVRO3iE
There's a related bug report here. I've found a workaround that, from limited testing, seems to do the trick with no repercussions. Add a custom implementation of your root ViewGroup (I almost always am using FrameLayout, so this is what I've tested with) with the logic below. Then, use this custom layout in place of your root layout, and ensure you set android:fitsSystemWindows="true". You can then just call getInsets() any time after layout (e.g. add an OnPreDrawListener) to adjust the rest of your layout to account for the system insets, if desired.
import android.content.Context;
import android.graphics.Rect;
import android.os.Build;
import android.util.AttributeSet;
import android.widget.FrameLayout;
import org.jetbrains.annotations.NotNull;
/**
* #author Kevin
* Date Created: 3/7/14
*
* https://code.google.com/p/android/issues/detail?id=63777
*
* When using a translucent status bar on API 19+, the window will not
* resize to make room for input methods (i.e.
* {#link android.view.WindowManager.LayoutParams#SOFT_INPUT_ADJUST_RESIZE} and
* {#link android.view.WindowManager.LayoutParams#SOFT_INPUT_ADJUST_PAN} are
* ignored).
*
* To work around this; override {#link #fitSystemWindows(android.graphics.Rect)},
* capture and override the system insets, and then call through to FrameLayout's
* implementation.
*
* For reasons yet unknown, modifying the bottom inset causes this workaround to
* fail. Modifying the top, left, and right insets works as expected.
*/
public final class CustomInsetsFrameLayout extends FrameLayout {
private int[] mInsets = new int[4];
public CustomInsetsFrameLayout(Context context) {
super(context);
}
public CustomInsetsFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomInsetsFrameLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public final int[] getInsets() {
return mInsets;
}
#Override
protected final boolean fitSystemWindows(#NotNull Rect insets) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// Intentionally do not modify the bottom inset. For some reason,
// if the bottom inset is modified, window resizing stops working.
// TODO: Figure out why.
mInsets[0] = insets.left;
mInsets[1] = insets.top;
mInsets[2] = insets.right;
insets.left = 0;
insets.top = 0;
insets.right = 0;
}
return super.fitSystemWindows(insets);
}
}
Since fitSystemWindows was deprecated, please refer to the answer below to complete the workaround.
#kcoppock answer is really helpful, but fitSystemWindows was deprecated in API level 20
So since API 20 (KITKAT_WATCH) you should override onApplyWindowInsets
#Override
public final WindowInsets onApplyWindowInsets(WindowInsets insets) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
return super.onApplyWindowInsets(insets.replaceSystemWindowInsets(0, 0, 0,
insets.getSystemWindowInsetBottom()));
} else {
return insets;
}
}
If you want to customize the insets and you are targeting API level >=21 you can accomplish this without having to create a custom view group. By just setting fitsSystemWindows padding will be applied to your container view by default, which you may not want.
The version checks are built into this method and only devices >= 21 will execute the code inside the lambda. Kotlin example:
ViewCompat.setOnApplyWindowInsetsListener(container) { view, insets ->
insets.replaceSystemWindowInsets(0, 0, 0, insets.systemWindowInsetBottom).apply {
ViewCompat.onApplyWindowInsets(view, this)
}
}
Make sure your layout still sets the fitsSystemWindows flag otherwise the window insets listener will not be called.
<FrameLayout
android:id="#+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
/>
These sources are helpful:
https://medium.com/google-developers/why-would-i-want-to-fitssystemwindows-4e26d9ce1eec
https://medium.com/#azizbekian/windowinsets-24e241d4afb9
This worked for me to have translucent status bar and adjustResize in fragment:
Make a custom RelativeLayout as #Victor91 and #kcoppock said.
Use CustomRelativeLayout as parent layout for your fragment.
Declare theme with android:windowTranslucentStatus = true
The container Activity must be declared in Manifest with
android:windowSoftInputMode="adjustResize" and use the declared
theme
Please Use fitsSystemWindows on fragment root layout!
public class CustomRelativeLayout extends RelativeLayout {
private int[] mInsets = new int[4];
public CustomRelativeLayout(Context context) {
super(context);
}
public CustomRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public CustomRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
#Override
public final WindowInsets onApplyWindowInsets(WindowInsets insets) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
mInsets[0] = insets.getSystemWindowInsetLeft();
mInsets[1] = insets.getSystemWindowInsetTop();
mInsets[2] = insets.getSystemWindowInsetRight();
return super.onApplyWindowInsets(insets.replaceSystemWindowInsets(0, 0, 0,
insets.getSystemWindowInsetBottom()));
} else {
return insets;
}
}
}
Then in xml,
<com.blah.blah.CustomRelativeLayout 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"
android:fitsSystemWindows="true">
</com.blah.blah.CustomRelativeLayout>
A small update on the helpful #Victor Rendina's answer caused by the replaceSystemWindowInsets and systemWindowInsetBottom methods deprecation.
Prerequisites:
API >= 21
implementation 'androidx.core:core-ktx:1.5.0-alpha02' at least
Kotlin extension:
fun View?.fitSystemWindowsAndAdjustResize() = this?.let { view ->
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
view.fitsSystemWindows = true
val bottom = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
WindowInsetsCompat
.Builder()
.setInsets(
WindowInsetsCompat.Type.systemBars(),
Insets.of(0, 0, 0, bottom)
)
.build()
.apply {
ViewCompat.onApplyWindowInsets(v, this)
}
}
}
Usage:
rootView.fitSystemWindowsAndAdjustResize()
where rootView is literally the root view of the layout :)
Note: if the extension does not work for your root view (I ran into this when having ConstraintLayout as the rootView) wrap the entire layout with a FrameLayout so that the FrameLayout becomes the new root view.
I had the same problem,
My Activity had a ScrollView as root view and with translucent statusbar activated it didn't resize correctly when keyboard showed... and conseguently the screen didn't scrolled hiding the input views.
Solution:
Moved everything (layout and activity logic) inside a new Fragment.
Then changed the Activity to only include this Fragment. Now everything works as expected!
This is the layout of the activity:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/contentView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true" />
Add this first at your root layout.
android:fitsSystemWindows="true"
When you use this approach, it becomes your responsibility to ensure that critical parts of your app's UI (for example, the built-in controls in a Maps application) don't end up getting covered by system bars. This could make your app unusable. In most cases you can handle this by adding the android:fitsSystemWindows attribute to your XML layout file, set to true. This adjusts the padding of the parent ViewGroup to leave space for the system windows. This is sufficient for most applications.
In some cases, however, you may need to modify the default padding to get the desired layout for your app. To directly manipulate how your content lays out relative to the system bars (which occupy a space known as the window's "content insets"), override fitSystemWindows(Rect insets). The fitSystemWindows() method is called by the view hierarchy when the content insets for a window have changed, to allow the window to adjust its content accordingly. By overriding this method you can handle the insets (and hence your app's layout) however you want.
https://developer.android.com/training/system-ui/status#behind
If you want to become a master window fitter, please watch the video from the android developer.
https://www.youtube.com/watch?v=_mGDMVRO3iE
Based on Joseph Johnson's workaround in Android How to adjust layout in Full Screen Mode when softkeyboard is visible
call this in onCreate() after setContentView() in your activity.
AndroidBug5497Workaround.assistActivity(this);
a litte different from original replace return (r.bottom - r.top); with return r.bottom; in computeUsableHeight()
for some reason, i must set my activity fitsSystemWindows attribute to false.
this workaround saved me. it's works well for me. hope can help you.
the implementation class is:
public class AndroidBug5497Workaround {
// For more information, see https://code.google.com/p/android/issues/detail?id=5497
// To use this class, simply invoke assistActivity() on an Activity that already has its content view set.
public static void assistActivity (Activity activity) {
new AndroidBug5497Workaround(activity);
}
private View mChildOfContent;
private int usableHeightPrevious;
private FrameLayout.LayoutParams frameLayoutParams;
private AndroidBug5497Workaround(Activity activity) {
FrameLayout content = (FrameLayout) activity.findViewById(android.R.id.content);
mChildOfContent = content.getChildAt(0);
mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
public void onGlobalLayout() {
possiblyResizeChildOfContent();
}
});
frameLayoutParams = (FrameLayout.LayoutParams) mChildOfContent.getLayoutParams();
}
private void possiblyResizeChildOfContent() {
int usableHeightNow = computeUsableHeight();
if (usableHeightNow != usableHeightPrevious) {
int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight();
int heightDifference = usableHeightSansKeyboard - usableHeightNow;
if (heightDifference > (usableHeightSansKeyboard/4)) {
// keyboard probably just became visible
frameLayoutParams.height = usableHeightSansKeyboard - heightDifference;
} else {
// keyboard probably just became hidden
frameLayoutParams.height = usableHeightSansKeyboard;
}
mChildOfContent.requestLayout();
usableHeightPrevious = usableHeightNow;
}
}
private int computeUsableHeight() {
Rect r = new Rect();
mChildOfContent.getWindowVisibleDisplayFrame(r);
return r.bottom;
}
}
It shouldn't work with the translucent status bar; that setting forces the window into fullscreen mode which does not work with adjustResize.
You can either use adjustPan or use the fitsSystemWindows properties. I would suggest reading about the feature though, it has significant side effects:
https://medium.com/google-developers/why-would-i-want-to-fitssystemwindows-4e26d9ce1eec
I had like a problem.
I set windowDrawsSystemBarBackgrounds to 'true' and my app should show under status bar.
It's my activity theme.
<item name="android:windowTranslucentStatus" tools:targetApi="KITKAT">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:windowTranslucentNavigation">true</item>
<item name="android:statusBarColor">#android:color/transparent</item>
and I got help from jianshu's blog.
you can read code but text like me.
I add few code more.
public final class ZeroInsetsFrameLayout extends FrameLayout {
private int[] mInsets = new int[4];
public ZeroInsetsFrameLayout(Context context) {
super(context);
}
public ZeroInsetsFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ZeroInsetsFrameLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public final int[] getInsets() {
return mInsets;
}
#Override
public WindowInsets computeSystemWindowInsets(WindowInsets in, Rect outLocalInsets) {
outLocalInsets.left = 0;
outLocalInsets.top = 0;
outLocalInsets.right = 0;
return super.computeSystemWindowInsets(in, outLocalInsets);
}
#Override
protected final boolean fitSystemWindows(#NonNull Rect insets) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// Intentionally do not modify the bottom inset. For some reason,
// if the bottom inset is modified, window resizing stops working.
// TODO: Figure out why.
mInsets[0] = insets.left;
mInsets[1] = insets.top;
mInsets[2] = insets.right;
insets.left = 0;
insets.top = 0;
insets.right = 0;
}
return super.fitSystemWindows(insets);
}
}
This is my fragment layout.
<com.dhna.widget.ZeroInsetsFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:background="#color/white">
<!-- your xml code -->
</ZeroInsetsFrameLayout>
I want it to be helpful to you.
good luck!
AndroidBug5497Workaround.java take care memory leak. need below code
getViewTreeObserver().removeOnGlobalLayoutListener(listener);
My sample using RxJava that automatically call removeOnGlobalLayoutListener() when onPause() in Activity's lifecycle
public class MyActivity extends RxAppCompatActivity {
// ...
protected void onStart(){
super.onStart();
TRSoftKeyboardVisibility
.changes(this) // activity
.compose(this.<TRSoftKeyboardVisibility.ChangeEvent>bindUntilEvent(ActivityEvent.PAUSE))
.subscribe(keyboardEvent -> {
FrameLayout content = (FrameLayout) findViewById(android.R.id.content);
View firstChildView = content.getChildAt(0);
firstChildView.getLayoutParams().height = keyboardEvent.viewHeight();
firstChildView.requestLayout();
// keyboardEvent.isVisible = keyboard visible or not
// keyboardEvent.keyboardHeight = keyboard height
// keyboardEvent.viewHeight = fullWindowHeight - keyboardHeight
});
//...
}
package commonlib.rxjava.keyboard;
import android.app.Activity;
import android.view.View;
import android.widget.FrameLayout;
import kr.ohlab.android.util.Assert;
import rx.Observable;
public class TRSoftKeyboardVisibility {
public static Observable<ChangeEvent> changes(Activity activity) {
Assert.notNull(activity, "activity == null");
FrameLayout content = (FrameLayout) activity.findViewById(android.R.id.content);
View childOfContent = content.getChildAt(0);
return Observable.create(
new TRSoftKeyboardVisibilityEventOnSubscribe(childOfContent));
}
public static final class ChangeEvent {
private final int keyboardHeight;
private final boolean visible;
private final int viewHeight;
public static ChangeEvent create(boolean visible, int keyboardHeight,
int windowDisplayHeight) {
return new ChangeEvent(visible, keyboardHeight, windowDisplayHeight);
}
private ChangeEvent(boolean visible, int keyboardHeight, int viewHeight) {
this.keyboardHeight = keyboardHeight;
this.visible = visible;
this.viewHeight = viewHeight;
}
public int keyboardHeight() {
return keyboardHeight;
}
public boolean isVisible() {
return this.visible;
}
public int viewHeight() {
return viewHeight;
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ChangeEvent)) return false;
ChangeEvent that = (ChangeEvent) o;
if (keyboardHeight != that.keyboardHeight) return false;
if (visible != that.visible) return false;
return viewHeight == that.viewHeight;
}
#Override
public int hashCode() {
int result = keyboardHeight;
result = 31 * result + (visible ? 1 : 0);
result = 31 * result + viewHeight;
return result;
}
#Override
public String toString() {
return "ChangeEvent{" +
"keyboardHeight=" + keyboardHeight +
", visible=" + visible +
", viewHeight=" + viewHeight +
'}';
}
}
}
package commonlib.rxjava.keyboard;
import android.graphics.Rect;
import android.view.View;
import android.view.ViewTreeObserver;
import kr.ohlab.android.util.Assert;
import rx.Observable;
import rx.Subscriber;
import rx.android.MainThreadSubscription;
import timber.log.Timber;
public class TRSoftKeyboardVisibilityEventOnSubscribe
implements Observable.OnSubscribe<TRSoftKeyboardVisibility.ChangeEvent> {
private final View mTopView;
private int mLastVisibleDecorViewHeight;
private final Rect mWindowVisibleDisplayFrame = new Rect();
public TRSoftKeyboardVisibilityEventOnSubscribe(View topView) {
mTopView = topView;
}
private int computeWindowFrameHeight() {
mTopView.getWindowVisibleDisplayFrame(mWindowVisibleDisplayFrame);
return (mWindowVisibleDisplayFrame.bottom - mWindowVisibleDisplayFrame.top);
}
private TRSoftKeyboardVisibility.ChangeEvent checkKeyboardVisibility() {
int windowFrameHeightNow = computeWindowFrameHeight();
TRSoftKeyboardVisibility.ChangeEvent event = null;
if (windowFrameHeightNow != mLastVisibleDecorViewHeight) {
int mTopViewHeight = mTopView.getHeight();
int heightDiff = mTopViewHeight - windowFrameHeightNow;
Timber.e("XXX heightDiff=" + heightDiff);
if (heightDiff > (mTopViewHeight / 4)) {
event = TRSoftKeyboardVisibility.ChangeEvent.create(true, heightDiff, windowFrameHeightNow);
} else {
event = TRSoftKeyboardVisibility.ChangeEvent.create(false, 0, windowFrameHeightNow);
}
mLastVisibleDecorViewHeight = windowFrameHeightNow;
return event;
}
return null;
}
public void call(final Subscriber<? super TRSoftKeyboardVisibility.ChangeEvent> subscriber) {
Assert.checkUiThread();
final ViewTreeObserver.OnGlobalLayoutListener listener =
new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
TRSoftKeyboardVisibility.ChangeEvent event = checkKeyboardVisibility();
if( event == null)
return;
if (!subscriber.isUnsubscribed()) {
subscriber.onNext(event);
}
}
};
mTopView.getViewTreeObserver().addOnGlobalLayoutListener(listener);
subscriber.add(new MainThreadSubscription() {
#Override
protected void onUnsubscribe() {
mTopView.getViewTreeObserver().removeOnGlobalLayoutListener(listener);
}
});
}
}
After I had researched on all forum. thoese ways can not help find point out. Lucky when i tried doing this way. It helps me resolved problem
XML
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<!-- Your xml -->
</RelativeLayout>
Activity
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView("Your Activity");
setAdjustScreen();
}
Created Func
protected void setAdjustScreen(){
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);
/*android:windowSoftInputMode="adjustPan|adjustResize"*/
}
Finally adding some lines to your mainifest
<activity
android:name="Your Activity"
android:windowSoftInputMode="adjustPan|adjustResize"
android:screenOrientation="portrait"></activity>
I had the same problem. I have solved using coordinatorlayout
activity.main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
android:layout_height="match_parent" android:layout_width="match_parent"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<android.support.design.widget.AppBarLayout
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:theme="#style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:layout_height="?attr/actionBarSize"
android:layout_width="match_parent"
app:popupTheme="#style/AppTheme.PopupOverlay"
android:background="?attr/colorPrimary"
android:id="#+id/toolbar"/>
</android.support.design.widget.AppBarLayout>
<include layout="#layout/content_main2"/>
</android.support.design.widget.CoordinatorLayout>
content_main2.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<android.support.v7.widget.RecyclerView
android:layout_height="match_parent"
android:layout_width="match_parent"
android:layout_marginTop="30dp"
android:layout_marginBottom="30dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
android:id="#+id/post_msg_recyclerview">
</android.support.v7.widget.RecyclerView>
<EditText
android:layout_width="match_parent"
android:layout_height="50dp"
app:layout_constraintBottom_toBottomOf="parent"
android:background="#color/colorPrimary"
/>
</android.support.constraint.ConstraintLayout>
MainActivity.java
now add this line linearLayoutManager.setStackFromEnd(true);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
linearLayoutManager.setStackFromEnd(true);
recyclerView.setLayoutManager(linearLayoutManager);
Adapter adapter1=new Adapter(arrayList);
recyclerView.setAdapter(adapter1);
<androidx.constraintlayout.widget.ConstraintLayout
android:fitsSystemWindows="true">
<androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.appbar.AppBarLayout>
<com.google.android.material.appbar.CollapsingToolbarLayout/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView>
<Editext/>
<androidx.core.widget.NestedScrollView/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
here is what i use
in the main view in the xml file you will add this
android:animateLayoutChanges="true"
then in the "onCreate" function you will before every thing get the status bar size like this
int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0)
{
status_bar=getResources().getDimensionPixelSize(resourceId);
}
then finally in "onCreate" you will add this to update the size
main_view= findViewById(R.id.the_main);
main_view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener()
{
#Override
public void onGlobalLayout()
{
Rect r = new Rect();
View view = getWindow().getDecorView();
view.getWindowVisibleDisplayFrame(r);
if (Math.abs(old_size - r.height()) > 100)
{
ViewGroup.LayoutParams params = main_view.getLayoutParams();
params.height = r.height()+ status_bar ;
main_view.setLayoutParams(params);
}
old_size = r.height();
}
});
I don't why but option adjustResize doesn't work with fullscreen. I've just added titleBar and works ( android:theme="#style/AppTheme"). Insted that I use in code " getSupportActionBar().hide();"
<activity
android:name=".ChatActivity"
android:launchMode="singleInstance"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize"
android:theme="#style/AppTheme"
/>
The best practice allows user scroll content when the keyboard is shown.
So to add this functionality you need put your root layout inside the ScrollView and use windowSoftInputMode="adjustResize" activity method.
But if you want to use this functionality with <item name="android:windowTranslucentStatus">true</item>
flag on Android 5 content won't be scrollable and will overlaps with keyboard.
To solve this issue check this answer