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'
Related
I want to add an animated dot that has an oscillating type animation (this will actually be drawn on top of an image, if that makes any difference).
Here's a sample of what I mean:
Could this somehow be done with two images and an animation between them? I'm not too clear on that, so some sample code would be nice. (or a link to a tutorial).
Cheers and thanks in advance.
I'm not sure about Your exact requirements, but for me it looks like You need smth like extending 'ring' above the circle. I've tried to implement it using custom ViewGroup in order to have all that functionality encapsulated in some 'container'. Steps are below:
1) Add values/attrs.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="OscillatorAnimatedView">
<attr name="centerImage" format="reference" />
<attr name="oscillatorImage" format="reference" />
<attr name="oscillatorInterval" format="integer" />
<attr name="oscillatorMaxExtend" format="float" />
</declare-styleable>
</resources>
2) Add view to You layout file:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:custom="http://schemas.android.com/apk/res/com.alexstarc.tests"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.alexstarc.tests.views.OscillatorAnimatedView
android:id="#+id/oscillator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
custom:centerImage="#drawable/center"
custom:oscillatorImage="#drawable/circle" />
</RelativeLayout>
3) Add images for center and circle to Your drawables (below is just random examples from the internet, note thayt it should be png with transparency):
drawable/center
drawable/circle
4) Create Your view (in my case it's com.alexstarc.tests.views.OscillatorAnimatedView):
package com.ruinalst.performance.tests.views;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.ViewGroup;
import android.view.animation.BounceInterpolator;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import com.ruinalst.performance.tests.R;
/**
* Specific view to provide 'oscilllator' kind of animation using two input views
*/
public final class OscillatorAnimatedView extends RelativeLayout {
/* Internal constants, mostly for default values */
/** default oscillator interval */
private static final int DEFAULT_INTERVAL = 700;
/** default oscillator extend */
private static final float DEFAULT_EXTEND = 1.5f;
/** Image to be displayed at the center */
private ImageView mCenterImage = null;
/** Image to oscillate */
private ImageView mOscillatorImage = null;
/** Oscillator animation */
private AnimatorSet mAnimatorSet = null;
public OscillatorAnimatedView(final Context context, final AttributeSet attrs) {
super(context, attrs);
initAndCompose(attrs);
}
public OscillatorAnimatedView(final Context context, final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
initAndCompose(attrs);
}
/**
* Internal init function to init all additional data
* and compose child for this ViewGroup
*
* #param attrs {#link AttributeSet} with data from xml attributes
*/
private void initAndCompose(final AttributeSet attrs) {
if (null == attrs) {
throw new IllegalArgumentException("Attributes should be provided to this view," +
" at least centerImage and oscillatorImage should be specified");
}
final TypedArray a = getContext().obtainStyledAttributes(attrs,
R.styleable.OscillatorAnimatedView, 0, 0);
final Drawable centerDrawable = a.getDrawable(R.styleable.OscillatorAnimatedView_centerImage);
final Drawable oscillatorDrawable = a.getDrawable(R.styleable.OscillatorAnimatedView_oscillatorImage);
if (null == centerDrawable || null == oscillatorDrawable) {
throw new IllegalArgumentException("Attributes should be provided to this view," +
" at least centerImage and oscillatorImage should be specified");
}
final int oscillatorInterval = a.getInt(R.styleable.OscillatorAnimatedView_oscillatorInterval, DEFAULT_INTERVAL);
final float maxOscillatorExtend = a.getFloat(R.styleable.OscillatorAnimatedView_oscillatorMaxExtend, DEFAULT_EXTEND);
a.recycle();
// Create child and add them into this view group
mCenterImage = new ImageView(getContext());
mCenterImage.setImageDrawable(centerDrawable);
addInternalChild(mCenterImage);
mOscillatorImage = new ImageView(getContext());
mOscillatorImage.setImageDrawable(oscillatorDrawable);
addInternalChild(mOscillatorImage);
// Init animation
mAnimatorSet = new AnimatorSet();
mAnimatorSet.setDuration(oscillatorInterval);
final ObjectAnimator scaleXAnimator = ObjectAnimator.ofFloat(mOscillatorImage, "ScaleX", 1.0f, maxOscillatorExtend);
scaleXAnimator.setRepeatCount(ObjectAnimator.INFINITE);
scaleXAnimator.setRepeatMode(ObjectAnimator.INFINITE);
final ObjectAnimator scaleYAnimator = ObjectAnimator.ofFloat(mOscillatorImage, "ScaleY", 1.0f, maxOscillatorExtend);
scaleYAnimator.setRepeatCount(ObjectAnimator.INFINITE);
scaleYAnimator.setRepeatMode(ObjectAnimator.INFINITE);
mAnimatorSet.playTogether(scaleXAnimator, scaleYAnimator);
mAnimatorSet.setInterpolator(new BounceInterpolator());
}
/**
* Internal helper to add child view to this ViewGroup.
* Used currently only for two internal ImageViews
*
* #param child {#link ImageView} to be added
*/
private void addInternalChild(final ImageView child) {
final LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
params.addRule(CENTER_IN_PARENT, 1);
addView(child, params);
}
/**
* Starts animation for this view
*/
public void start() {
mAnimatorSet.start();
}
/**
* Stops animation for this view
*/
public void stop() {
mAnimatorSet.end();
}
}
5) In Your activity do smth like:
#Override
protected void onResume() {
super.onResume();
mOscillatorView.start();
}
#Override
protected void onPause() {
super.onPause();
mOscillatorView.stop();
}
Please note, that it's not release version and it most probably can be improved in many ways.
You can also play with interpolators or create Your own in order to have expected animation.
Are you looking for a Drawable Animation? Seems like that would do what you're looking for. You can use a RelativeLayout to position it on top of your other views.
Also, if you need more complex animation, you could use a SurfaceView and paint the canvas to your linking.
Override the onDraw method and draw a first circle for your button, also, create a boolean variable to control when your button is going to pulse and when is not.Finally draw your second circle with an alpha as a background. Making a pulsating effect:
#Override
protected void onDraw(Canvas canvas) {
int w = getMeasuredWidth();
int h = getMeasuredHeight();
//Draw circle
canvas.drawCircle(w/2, h/2, MIN_RADIUS_VALUE , mCirclePaint);
if (mAnimationOn) {
if (mRadius >= MAX_RADIUS_VALUE)
mPaintGoBack = true;
else if(mRadius <= MIN_RADIUS_VALUE)
mPaintGoBack = false;
//Draw pulsating shadow
canvas.drawCircle(w / 2, h / 2, mRadius, mBackgroundPaint);
mRadius = mPaintGoBack ? (mRadius - 0.5f) : (mRadius + 0.5f);
invalidate();
}
super.onDraw(canvas);
}
public void animateButton(boolean animate){
if (!animate)
mRadius = MIN_RADIUS_VALUE;
mAnimationOn = animate;
invalidate();
}
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 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.
How can I make the textview wrap such text exactly ?
android:width attribute is not a solution, because the text is dynamic.
Desired behaviour
|Adcs |
|adscfd|
Current behavour:
|Adcs |
|adscfd |
Hereis the code (styles of TextViews only define things like textColor, textSize, textStyle).
<TextView
android:id="#+id/text_title_holder"
style="#style/TextBold.Black.Title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:maxWidth="100dp"
android:maxLines="2"
android:text="Adcs adscfd"
android:gravity="left"
android:visibility="visible" />
The topic wrap_content width on mutiline TextView has no good answer.
I have faced this problem and didn't find the solution in internet. I did this trick by creating the new component TightTextView that remeasures the given text in case you have specified the maxWidth of the component and the width of Layout (of text) is less that the measured width of the view.
package com.client.android.app.views;
import android.content.Context;
import android.text.Layout;
import android.util.AttributeSet;
import android.widget.TextView;
/**
* Tightly wraps the text when setting the maxWidth.
* #author sky
*/
public class TightTextView extends TextView {
private boolean hasMaxWidth;
public TightTextView(Context context) {
this(context, null, 0);
}
public TightTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TightTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (hasMaxWidth) {
int specModeW = MeasureSpec.getMode(widthMeasureSpec);
if (specModeW != MeasureSpec.EXACTLY) {
Layout layout = getLayout();
int linesCount = layout.getLineCount();
if (linesCount > 1) {
float textRealMaxWidth = 0;
for (int n = 0; n < linesCount; ++n) {
textRealMaxWidth = Math.max(textRealMaxWidth, layout.getLineWidth(n));
}
int w = Math.round(textRealMaxWidth);
if (w < getMeasuredWidth()) {
super.onMeasure(MeasureSpec.makeMeasureSpec(w, MeasureSpec.AT_MOST),
heightMeasureSpec);
}
}
}
}
}
#Override
public void setMaxWidth(int maxpixels) {
super.setMaxWidth(maxpixels);
hasMaxWidth = true;
}
#Override
public void setMaxEms(int maxems) {
super.setMaxEms(maxems);
hasMaxWidth = true;
}
}
!!! Just did port it to older android APIs, cuz getMaxWidth() is only available since API level 16.
This question is a little old now but I too had this problem where I wanted green text in a black box over a mapView and got around it by putting my textView in a RelativeLayout container. I then used padding to set the border size. The textView now hugs the text nicely.
My outline in eclipse looks like this.
RelativeLayout
mapview
LinearLayout
RelativeLayout
textView1 << this is the box I want to hug the text
imageView1
RelativeLayout
etc....
Hope this helps.
I found how to change the opacity of a View, but I need to actually darken a View. My best idea is to put a transparent black rectangle over it and then slowly increase the opacity of the rectangle.
Do you know a nicer way to do it?
public class Page07AnimationView extends ParentPageAnimationView {
private final String TAG = this.getClass().getSimpleName();
private ImageView overlay;
private int mAlpha = 0;
public Page07AnimationView(Context context) {
super(context);
}
public Page07AnimationView(Context context, AttributeSet attrs) {
super(context, attrs);
}
protected void init()
{
overlay = new ImageView(mContext);
overlay.setImageResource(R.drawable.black_background);
overlay.setAlpha(0);
overlay.setWillNotDraw(false);
// make the PageAniSurfaceView focusable so it can handle events
setFocusable(true);
}
protected void draw_bitmaps(Canvas canvas)
{
overlay.draw(canvas);
update_bitmaps();
invalidate();
}
public void update_bitmaps()
{
if(mAlpha < 250)
{
mAlpha += 10;
overlay.setAlpha(mAlpha);
}
}
}
The code above isn't doing what I had hoped. Page07AnimationView is added to a FrameLayout over the view I need to darken. R.drawable.black_background points to a 787px x 492px black png image.
I added overlay.setWillNotDraw(false); but it didn't help.
I changed the first setAlpha(0) to setAlpha(255) but that didn't help.
I removed the setAlpha() calls altogether, but it didn't help.
This basic technique of adding a PageNNAnimationView has been working to draw Bitmaps, but not to draw ImageView overlay. (I would use Bitmaps, but they don't seem to have an alpha component.)
Edit2: this is the parent of the class above:
public class ParentPageAnimationView extends View {
private final String TAG = this.getClass().getSimpleName();
protected Context mContext;
public ParentPageAnimationView(Context context) {
super(context);
mContext = context;
init();
}
public ParentPageAnimationView(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
init();
}
protected void init()
{
}
protected void draw_bitmaps(Canvas canvas)
{
// will be overridden by child classes
}
#Override
protected void onDraw(Canvas canvas) {
if(this.getVisibility() == View.VISIBLE)
{
if(canvas != null)
{
draw_bitmaps(canvas);
}
}
}
public void update_bitmaps()
{
// will be overridden by child classes
}
public void elementStarted(PageElement _pageElement) {
// Nothing in parent class
}
public void elementFinished(PageElement mElement) {
// Nothing in parent class
}
}
In case of an ImageView, here's one way to achieve it:
imageView.setColorFilter(Color.rgb(123, 123, 123), android.graphics.PorterDuff.Mode.MULTIPLY);
I would rather do it in the opposite way - put a dark rectangle behind the view and set the view's opacity. This saves painting the rectangle when the view is 100% opaque.
I would do something like this:
view.getBackground().setColorFilter(color, PorterDuff.Mode.DARKEN);
Use black color with some alpha like 0x7f000000 for a typical darkening.
It's more concise and you can also darken the View with animation or scrolling event for example. Just set Color.argb(alpha, 0, 0, 0) as the color and animate alpha, or change it based on the scrolling offset.
This is how I ended up doing it. The key was to use a Paint with its alpha set to whatever I wanted.
public class Page07AnimationView extends ParentPageAnimationView {
private final String TAG = this.getClass().getSimpleName();
private Bitmap bitmap;
private BitmapDrawable drawable;
private ImageView overlay;
private int which = -1;
private long last_time;
private Page07State state;
private int mAlpha;
private int maxAlpha;
private Paint mPaint;
private int _alpha_step;
private int minAlpha;
public enum Page07State {
WAITING, DARKENING, DARKENED
}
public Page07AnimationView(Context context) {
super(context);
}
public Page07AnimationView(Context context, AttributeSet attrs) {
super(context, attrs);
}
protected void init()
{
minAlpha = 0;
mAlpha = minAlpha;
_alpha_step = 5;
maxAlpha = 255;
mPaint = new Paint();
mPaint.setAlpha(minAlpha);
state = Page07State.WAITING;
overlay = new ImageView(mContext);
overlay.setImageResource(R.drawable.black_background);
drawable = (BitmapDrawable) overlay.getDrawable();
bitmap = drawable.getBitmap();
last_time = 0;
}
protected void draw_bitmaps(Canvas canvas)
{
if(state != Page07State.WAITING)
{
DebugLog.d(TAG, "drawing " + Integer.toString(which));
canvas.drawBitmap(bitmap, 0, 0, mPaint);
}
update_bitmaps();
invalidate();
}
public void update_bitmaps()
{
if(state == Page07State.DARKENING)
{
if(mAlpha < maxAlpha)
{
if(System.currentTimeMillis() > last_time + 12)
{
last_time = System.currentTimeMillis();
mAlpha += _alpha_step;
mPaint.setAlpha(mAlpha);
}
}
else
{
state = Page07State.DARKENED;
}
}
}
public void runAnimation()
{
state = Page07State.DARKENING;
}
}
Adding to android developer's answer:
imageView.setColorFilter(Color.rgb(123, 123, 123), android.graphics.PorterDuff.Mode.MULTIPLY);
you can setColorFilter on any view like this:
GradientDrawable gd = (GradientDrawable) textView.getBackground();
gd.setColor(color); //you can also set BG color to a textview like this
gd.setColorFilter(Color.rgb(123, 123, 123), android.graphics.PorterDuff.Mode.MULTIPLY);
you could try using the Alpha animation like this (perhaps on the rectangle):
Animation animation = new AlphaAnimation(0.0f, 1.0f);
animation.setDuration(350);
That would cause the rectangle to gradually become opaque over 350 seconds...
Android actually exposes a drawable which can be used to darken views. You can easily attach it to any view with an Overlay.
Here are two extension functions which can be used to darken any view.
fun View.darken() {
val darkOverlay = ResourcesCompat.getDrawable(
resources,
android.R.drawable.screen_background_dark_transparent,
context.theme
)!!.mutate() // We mutate the drawable so we can later implement a fade in/out animation and animate the Drawable's alpha property. Since Drawables share their state we need to mutate otherwise we would impact all instances of this drawable
darkOverlay.setBounds(0, 0, width, height)
setTag(R.id.dark_overlay, darkOverlay)
overlay.add(darkOverlay)
}
fun View.lighten() {
(getTag(R.id.dark_overlay) as? Drawable)?.let {
overlay.remove(it)
setTag(R.id.dark_overlay, null)
}
}
Make sure you add the id to ids.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="dark_overlay" type="id" />
</resources>
And if you're darkening your application's root layout and would like to darken the NavigationBar as well, you might need to add the the following to your theme in styles.xml
<style name="BaseTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- required for api 29 otherwise the system will set a white background color to the NavigationBar to ensure the buttons are visible -->
<item name="android:enforceNavigationBarContrast">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
</style>
You should check iPaulPro's answer in this question. You will need to extend ImageView and override the onDraw() method.
Depending on what you are going to do, Alexandru Cristescu's answer is also valid but you should
call setFillAter(true) for the animation to persist after finished.