I am trying to implement an animation on the views which is basically behind my RecyclerView. I am using ItemTouchHelper.SimpleCallback class to get the motion events on RecyclerView and overriding the callback method onChildDraw() to propagate the animation.
This animation is basically controlled by user swipe gestures. So here is my problem.
I have a RecyclerView with a email content on a card view. There are two images on the left and right side of the screen below that card view. It is something like this shown in the image inside this link:
CardView inside a RecyclerView with left and right images behind card view
Initially, Red and Pink images are invisible and will be animated on the amount of swipe on the white cardview. Now when I swipe on the white card view to the left and right hand side, the images in red and pink will be shown up by scale, alpha animation. Scale animation starts from 0.8 and alpha animation starts from 0 to 255. When these two animation finishes the Red and Pink images will be translated towards the direction of the swipe.
Here is my code:
public class ViewItemTouchHelperCallback extends ItemTouchHelper.SimpleCallback {
private Context mContext;
private OnViewSwipedListener mOnSwipeListener;
public interface OnViewSwipedListener {
void onViewSwiped(float dX);
}
public FocusViewItemTouchHelperCallback(int dragDirs, int swipeDirs, Context context, OnViewSwipedListener listener) {
super(dragDirs, swipeDirs);
this.mContext = context;
this.mOnSwipeListener = listener;
}
#Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
return false;
}
#Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {
if(mContext instanceof ViewActivity) {
if(viewHolder != null) {
WebView webView = ((ViewAdapter.MailObjectHolder) viewHolder).webViewBody;
if(webView != null) {
webView.clearView();
webView.loadUrl(Constants.ABOUT_BLANK);
if(swipeDir == ItemTouchHelper.LEFT) {
((ViewActivity) mContext).onSwipedLeft();
} else if(swipeDir == ItemTouchHelper.RIGHT) {
((ViewActivity) mContext).onSwipedRight();
}
}
}
}
}
#Override
public void onChildDraw(Canvas canvas, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
if(actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
if(mOnSwipeListener != null) {
mOnSwipeListener.onViewSwiped(dX);
}
super.onChildDraw(canvas, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
}
}
Below is my activity code where I get the callbacks from the above class:
#Override
public void onViewSwiped(float dX) {
if(dX == 0) {
mFlagEmailIcon.setVisibility(View.INVISIBLE);
mNextEmailIcon.setVisibility(View.INVISIBLE);
} else if(dX > 0) {
//Left to right - flag email
float swipedWidth = Math.abs(dX) / mFlagEmailIcon.getMeasuredWidth();
if(swipedWidth >= 1) {
LogUtils.log(TAG, "Left to right dX = " + dX + "and Swiped width = " + swipedWidth);
mFlagEmailIcon.setVisibility(View.VISIBLE);
float scaleChange = Math.scalb(Math.abs(dX) / mFlagEmailIcon.getMeasuredWidth(), 1);
if(scaleChange >= 0.8 && scaleChange <= 1) {
mFlagEmailIcon.animate().scaleX(scaleChange).scaleY(scaleChange);
}
mFlagEmailIcon.animate().alpha(scaleChange);
} else {
}
} else {
//Right to left - next email
float swipedWidth = Math.abs(dX) / mNextEmailIcon.getMeasuredWidth();
if(swipedWidth >= 1) {
LogUtils.log(TAG, "Right to left dX = " + dX + "and Swiped width = " + swipedWidth);
mNextEmailIcon.setVisibility(View.VISIBLE);
float scaleChange = Math.scalb(Math.abs(dX) / mNextEmailIcon.getMeasuredWidth(), 1);
if(scaleChange >= 0.8 && scaleChange <= 1) {
mNextEmailIcon.animate().scaleX(scaleChange).scaleY(scaleChange);
}
mNextEmailIcon.animate().alpha(scaleChange);
} else {
}
}
}
In the above code, I am trying to animate the Red and Pink image based on the amount of user swipe on the white card view. But I am really stuck in the calculation based on "dX" and can't figure out how to calculate the values for animation.
Note:- When user swipes left to right on the card then Red image
should be animated and when user swipes right to left on the card then
Pink image should be animated. Animation will contain scale and alpha
animation initially and when both animation finishes the Red or Pink
image will start translating towards the direction of the swipe.
Can anyone help me to calculate the animation values based on user swipe. If you have any query related to this question, please ask. I am really stucked in this problem and there is no solution for this anywhere on the web.
Any help will be appreciated. Thanks in advance.
Related
I want a text inside a textbox to scroll to end and then restart.
Using marquee scrolls the whole text until the first letter reaches the "start point", but I want the text only to scroll until the last letter of the text becomes visible, then pause and the text should jump to start and start scrolling again.
Is there a way to do this. I have search but I can't find something that works for me.
Thanks,
Sebi
I am not aware of any way to configure a TextView to take on the marquee behavior that you are seeking. The Stack Overflow Q/A I referred you to in the comments does translate the entire TextView. I think that this will work as a marquee if you set the TextView within a ViewGroup such that the TextView is clipped. You can also do you own clipping. You would have to try this to see if it would work. If this does work, it may be sufficient for your needs, but you may be missing the fading edge and maybe some other characteristics of true marquee scrolling. Also, some other characteristics of the TextView such as shadows, borders and backgrounds might not look right.
The contents of the TextView is animated by the TextView code itself through canvas translation for marquee scrolling. See here.
if (isMarqueeFadeEnabled()) {
if (!mSingleLine && getLineCount() == 1 && canMarquee()
&& (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) {
final int width = mRight - mLeft;
final int padding = getCompoundPaddingLeft() + getCompoundPaddingRight();
final float dx = mLayout.getLineRight(0) - (width - padding);
canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
}
if (mMarquee != null && mMarquee.isRunning()) {
final float dx = -mMarquee.getScroll();
canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
}
}
You can also consider writing a custom TextView and doing your own text scrolling. You can look to the TextView code itself for some tips on how you might do it. I think writing your own TextView would have better results. (IMO)
I have rewrite the Code from the GitHub Project here (which was posted in the comment from Cheticamp) to java and shorten it to my needs. Now it starts the animation infinite.
Here the java code:
MarqueeTextView.java:
public class MarqueeTextView extends androidx.appcompat.widget.AppCompatTextView {
private final Scroller mScroller;
private int mDelay;
private int mDuration;
private final Handler mHandler = new Handler();
private final Runnable mAnimateRunnable = new Runnable() {
#Override
public void run() {
//check if Animation is needed otherwise check next cycle (if text has changed for example)
if (getTextViewWidthWithPadding() < getTextWidth()) {
mScroller.startScroll(getStartX(), 0, 0, 0, 0);
invalidate();
int direction = getLayout().getParagraphDirection(0);
mScroller.startScroll(getStartX(), 0, (getTextWidth() - getTextViewWidthWithPadding()) * direction, 0, mDuration);
}
mHandler.postDelayed(mAnimateRunnable, mDelay);
}
};
public MarqueeTextView(Context context) {
this(context, null);
}
public MarqueeTextView(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
mScroller = new Scroller(context, new LinearInterpolator());
setScroller(mScroller);
}
private int getTextWidth() {
String text = getText().toString();
TextPaint paint = getPaint();
Rect bounds = new Rect();
paint.getTextBounds(text, 0, text.length(), bounds);
return bounds.width();
}
private int getTextViewWidthWithPadding() {
return getWidth() - (getPaddingStart() + getPaddingEnd());
}
private int getStartX() {
boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
int lineRight = (int) getLayout().getLineRight(0);
if (isRtl) {
return lineRight - getTextViewWidthWithPadding();
} else {
return 0;
}
}
public void startAnimation(int delay, int duration) {
mDelay = delay;
mDuration = duration;
mHandler.postDelayed(mAnimateRunnable, delay);
}
}
activity_main.xml:
<MarqueeTextView
android:id="#+id/name"
android:layout_width="250dp"
android:layout_height="wrap_content"
android:ellipsize="none"
android:singleLine="true"
android:text="1. Simple text that shows how to use custom marquee"
/>
MainActivty.java:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
((MarqueeTextView)findViewById(R.id.name)).startAnimation(3000, 2000);
}
Using LayoutManager.findFirstCompletelyVisibleItemPosition(); I’m trying to zoom-in when the element is fully visible and zoom-out when it becomes partially/completely invisible. Scrolling over an object that is still visible completely slowly, the object position number is repeated several times along with the animation on the already animated object or in animation. My problem then is, stop this repetition and make the animation correct and unique. This is what I tried to do:
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
Animation scale_in = AnimationUtils.loadAnimation(recyclerView.getContext(), R.anim.scale_in_tv);
Animation scale_out = AnimationUtils.loadAnimation(recyclerView.getContext(), R.anim.scale_out_tv);
scale_in.setFillAfter(true);
//scale_out.setFillAfter(true);
int firstCompletelyVisible = layoutManager.findFirstCompletelyVisibleItemPosition();
int lastCompletelyVisible = layoutManager.findLastCompletelyVisibleItemPosition();
int before = -1;
View firstComp = layoutManager.findViewByPosition(firstCompletelyVisible);
View lastComp = layoutManager.findViewByPosition(lastCompletelyVisible);
if(firstComp != null && before != firstCompletelyVisible) {
before = firstCompletelyVisible;
firstComp.startAnimation(scale_out);
}
I have an app with a RecyclerView and in each item I implemented Swipe to star/dismiss behavior using onTouch(). There's no problem with my implementation and I can handle right and left swipe and claim drag from RecyclerView.
Each item is RevealFrameLayout that consists of content on the top, and below there is the original layout and the layout to be revealed, just like this:
Top layout:
Below layout (top revealed layer):
Below layout (default layout before reveal):
There I reveal the top revealed layers (colored ones) and everything works, until I added another ObjectAnimator to hide the star layout (make starred/unstarred), when The reveal works for the first time, and then another swipe will execute the hide animator, then 3rd time, where it's supposed to reveal once again, it really starts the animation (by debugging start() is executed) but it doesn't show up. 4th time the hiding animator works but no revealed layout.
The method to trigger any action:
void triggerAction(SwipeAction swipeAction, final Note note, final int position) {
if (swipeAction.getActionId() == SwipeAction.STAR) {
if (note.isStarred() && !hidden && !hideAnimating && !justAnimated) {
starActionLayout.setVisibility(View.VISIBLE);
archiveActionLayout.setVisibility(View.GONE);
Animator hideAnim = AnimationUtils.createHideAnimation(revealStar);
hideAnim.addListener(new AnimatorListenerAdapter() {
#Override
public void onAnimationEnd(Animator animation) {
revealStar.setVisibility(View.INVISIBLE);
hideAnimating = false;
hidden = true;
note.setStarred(false);
MainActivity.notesController.set(position, note);
super.onAnimationEnd(animation);
}
});
hideAnim.start();
revealStar.setVisibility(View.VISIBLE); // DEBUG: Only to make sure the star layout is shown.
justAnimated = true;
hideAnimating = true;
revealAnimating = false;
revealed = false;
hidden = false;
} else if (!note.isStarred() && !revealAnimating && !revealed && !justAnimated) {
starActionLayout.setVisibility(View.VISIBLE);
archiveActionLayout.setVisibility(View.GONE);
final ObjectAnimator revealAnim = (ObjectAnimator) AnimationUtils.createCircularReveal(revealStar, (int) getCenterX(starActionImage), (int) getCenterY(starActionImage), 0, (float) getRadius(starActionLayout));
revealAnim.addListener(new AnimatorListenerAdapter() {
#Override
public void onAnimationEnd(Animator animation) {
revealAnimating = false;
revealed = true;
note.setStarred(true);
MainActivity.notesController.set(position, note);
super.onAnimationEnd(animation);
}
});
revealAnim.start();
justAnimated = true;
revealAnimating = true;
hideAnimating = false;
hidden = false;
revealed = false;
revealStar.setVisibility(View.VISIBLE);
}
} else if (swipeAction.getActionId() == SwipeAction.ARCHIVE) {
if (!revealAnimating && !revealed) {
int added = starActionLayout.getWidth();
starActionLayout.setVisibility(View.GONE);
archiveActionLayout.setVisibility(View.VISIBLE);
Animator revealAnim = AnimationUtils.createCircularReveal(revealArchive, (int) getCenterX(archiveActionImage) + added, (int) getCenterY(archiveActionImage), 0, (float) getRadius(archiveActionLayout));
revealAnim.addListener(new AnimatorListenerAdapter() {
#Override
public void onAnimationEnd(Animator animation) {
revealAnimating = false;
revealed = true;
super.onAnimationEnd(animation);
}
});
revealAnim.start();
revealAnimating = true;
revealArchive.setVisibility(View.VISIBLE);
}
}
}
There's no problem with my conditions as debugging shows that the reveal animator show code is executed when it should. And AnimationUtils is a custom class that wraps ViewAnimationUtils.createCircularReveal(params) and another custom ObjectAnimator createHideAnimator() and there's no problem with that too.
AnimationUtils.java:
package com.skaldebane.util.graphics;
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.os.Build;
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.animation.LinearInterpolator;
public class AnimationUtils {
public static Animator createCircularReveal(View view, int centerX, int centerY, float startRadius, float endRadius) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) return ViewAnimationUtils.createCircularReveal(view, centerX, centerY, startRadius, endRadius);
else return io.codetail.animation.ViewAnimationUtils.createCircularReveal(view, centerX, centerY, startRadius, endRadius);
}
public static Animator createHideAnimation(View view) {
ObjectAnimator hideAnim = new ObjectAnimator();
hideAnim.setPropertyName("alpha");
hideAnim.setFloatValues(1.0F, 0.0F);
hideAnim.setDuration(300);
hideAnim.setInterpolator(new LinearInterpolator());
hideAnim.setTarget(view);
return hideAnim;
}
}
Note: Don't tell me to use ItemTouchHelper instead as that is not the subject of my question and it doesn't solve the problem either.
I just figured out the problem. It was simple but hard to notice. When I was applying the hide animator it was setting alpha permanently to 0, which meant the view would never appear again. I only had to use view.setAlpha(1.0F); directly after calling view.setVisiblity(View.INVISIBLE);.
I am using a glow effect which works using setMaskFilter to blur the painted area:
public static Paint createGlowPaint(Context context, #ColorRes int baseColorKey) {
float glowWidth = context.getResources().getDimension(R.dimen.analog_blur_width);
Paint paint = new Paint();
paint.setColor((Workarounds.getColor(context, baseColorKey) & 0xFFFFFF) | 0x40000000);
paint.setMaskFilter(new BlurMaskFilter(glowWidth, BlurMaskFilter.Blur.NORMAL));
paint.setStrokeWidth(glowWidth);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setStyle(Paint.Style.FILL_AND_STROKE);
return paint;
}
This is used along with custom vector graphics to do the drawing of the blur and then the drawing of the hands of my watch face. This is all packaged up into a Drawable so that I can use it in more than one place. Or so I thought! It turns out that even though this works fine when painted directly onto the top-level canvas, when I try to use ImageView#setImageDrawable to set the exact same Drawable onto an ImageView, the filter no longer gets applied.
You can see how this sort of blur looks:
Using it with an ImageView, now you get a hard edge instead:
What is going on here?
Edit for additional info:
Code that does the drawing, i.e. is using the glow paint:
public abstract class Hands extends Drawable {
// ... lots of other cruft
#Override
public void draw(Canvas canvas) {
//todo could probably precompute more in onBoundsChange
Rect bounds = getBounds();
int centerX = bounds.centerX();
int centerY = bounds.centerY();
if (watchMode.isInteractive()) {
handGlowPath.reset();
handGlowPath.addPath(hourHand.getPath());
handGlowPath.addPath(minuteHand.getPath());
handGlowPath.addCircle(centerX, centerY, centreRadius, Path.Direction.CCW);
canvas.drawPath(handGlowPath, handGlowPaint);
}
hourHand.draw(canvas);
minuteHand.draw(canvas);
if (watchMode.isInteractive()) {
secondHand.draw(canvas);
if (hasThirds()) {
thirdHand.draw(canvas);
}
}
canvas.drawCircle(centerX, centerY, centreRadius, centrePaint.getPaint());
}
Code that puts the drawable into the ImageView:
class PreviewListViewAdapter extends BaseAdapter implements ListAdapter {
// ... other methods for the list adapter
#Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView instanceof ViewHolder) {
holder = (ViewHolder) convertView;
} else {
holder = new ViewHolder(getContext(), inflater.inflate(R.layout.config_list_item, parent, false));
}
// Deliberately making this a square.
LinearLayout.LayoutParams holderLayoutParams =
new LinearLayout.LayoutParams(parent.getWidth(), parent.getWidth());
LinearLayout.LayoutParams viewLayoutParams =
new LinearLayout.LayoutParams(parent.getWidth(), parent.getWidth());
if (position == 0) {
holderLayoutParams.height += 20 * getResources().getDisplayMetrics().density;
viewLayoutParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
} else if (position == getCount() - 1) {
holderLayoutParams.height += 20 * getResources().getDisplayMetrics().density;
viewLayoutParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
} else {
viewLayoutParams.gravity = Gravity.CENTER;
}
holder.setLayoutParams(holderLayoutParams);
holder.view.setLayoutParams(viewLayoutParams);
holder.image.setImageDrawable(items[position].drawable);
holder.text.setText(items[position].labelText);
return holder;
}
}
Starting from API 14, BlurMaskFilters are not supported when hardware acceleration is enabled.
To work around this, set your ImageView's layer type to software:
holder.image.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
I am designing an android app, and one thing I would like to happen is when the user clicks the page, I would like to find the distance between where they clicked and a TextView element on the page. So far, what I have done is set the TextView ID by using
android:id="#+id/TableLayout01"
I know I also need to use android:onTouch coordinates somehow, but I am not sure how to incorporate this into the coordinates of my TextView
Thanks
You can put a touch listener on the whole activity and calculate the distance like this.
public void onCreate() {
setContentView(R.layout.somelayout);
...
TextView textView = findViewById(R.id.TableLayout01);
View mainView = getWindow().getDecorView().findViewById(android.R.id.content);
mainView.setonTouchListener(new View.onTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
int rawX = event.getRawX();
int rawY = event.getRawY();
int location[] = new int[2];
textView.getLocationOnScreen(location);
int distanceX = location[0] - rawX;
int distanceY = location[1] - rawY;
// Do something with the distance.
return false;
}
}
...
}