Skip to content

Commit

Permalink
Extension API improved to extend ViewRevealManagers
Browse files Browse the repository at this point in the history
added ViewTransformation API with default PathTransformation implementation
for view clipping

added ability to change ViewRevealManager in
RevealViewGroup#setViewRevealManager(ViewRevealManager)

added ability to change transformation method using
ViewRevealManager#setViewTransformation(ViewTransformation)
  • Loading branch information
ozodrukh committed Mar 16, 2017
1 parent 58bfe5b commit 7505025
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 72 deletions.
10 changes: 5 additions & 5 deletions circualreveal/build.gradle
@@ -1,15 +1,15 @@
apply plugin: 'com.android.library'
apply plugin: 'com.github.dcendents.android-maven'
apply plugin: 'maven'

group = 'com.github.ozodrukh'

android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
compileSdkVersion project.compileSDKVersion
buildToolsVersion project.buildToolsVersion

defaultConfig {
minSdkVersion 15
targetSdkVersion 23
minSdkVersion project.minSDKVersion
targetSdkVersion project.targetSDKVersion
}
}

Expand Down
Expand Up @@ -12,4 +12,10 @@ public interface RevealViewGroup {
* @return Bridge between view and circular reveal animation
*/
ViewRevealManager getViewRevealManager();

/**
*
* @param manager
*/
void setViewRevealManager(ViewRevealManager manager);
}
@@ -1,7 +1,6 @@
package io.codetail.animation;

import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.view.View;
import io.codetail.animation.ViewRevealManager.ChangeViewLayerTypeAdapter;
import io.codetail.view.BuildConfig;
Expand Down Expand Up @@ -67,20 +66,21 @@ public static Animator createCircularReveal(View view, int centerX, int centerY,
throw new IllegalArgumentException("Parent must be instance of RevealViewGroup");
}

RevealViewGroup viewGroup = (RevealViewGroup) view.getParent();
ViewRevealManager rm = viewGroup.getViewRevealManager();
final RevealViewGroup viewGroup = (RevealViewGroup) view.getParent();
final ViewRevealManager rm = viewGroup.getViewRevealManager();

if (!rm.hasCustomerRevealAnimator() && LOLLIPOP_PLUS) {
if (!rm.overrideNativeAnimator() && LOLLIPOP_PLUS) {
return android.view.ViewAnimationUtils.createCircularReveal(view, centerX, centerY,
startRadius, endRadius);
}

RevealValues viewData = new RevealValues(view, centerX, centerY, startRadius, endRadius);
ObjectAnimator animator = rm.createAnimator(viewData);
final RevealValues viewData = new RevealValues(view, centerX, centerY, startRadius, endRadius);
final Animator animator = rm.dispatchCreateAnimator(viewData);

if (layerType != view.getLayerType()) {
animator.addListener(new ChangeViewLayerTypeAdapter(viewData, layerType));
}

return animator;
}
}
175 changes: 117 additions & 58 deletions circualreveal/src/main/java/io/codetail/animation/ViewRevealManager.java
Expand Up @@ -14,78 +14,131 @@
import java.util.HashMap;
import java.util.Map;

@SuppressWarnings("WeakerAccess")
public class ViewRevealManager {
public static final ClipRadiusProperty REVEAL = new ClipRadiusProperty();

private Map<View, RevealValues> targets = new HashMap<>();
private final ViewTransformation viewTransformation;
private final Map<View, RevealValues> targets = new HashMap<>();
private final Map<Animator, RevealValues> animators = new HashMap<>();

private final AnimatorListenerAdapter animatorCallback = new AnimatorListenerAdapter() {
@Override public void onAnimationStart(Animator animation) {
final RevealValues values = getValues(animation);
values.clip(true);
}

@Override public void onAnimationCancel(Animator animation) {
endAnimation(animation);
}

@Override public void onAnimationEnd(Animator animation) {
endAnimation(animation);
}

private void endAnimation(Animator animation) {
final RevealValues values = getValues(animation);
values.clip(false);

// Clean up after animation is done
targets.remove(values.target);
animators.remove(animation);
}
};

public ViewRevealManager() {
this(new PathTransformation());
}

public ViewRevealManager(ViewTransformation transformation) {
this.viewTransformation = transformation;
}

protected ObjectAnimator createAnimator(RevealValues data) {
ObjectAnimator animator =
ObjectAnimator.ofFloat(data, REVEAL, data.startRadius, data.endRadius);
Animator dispatchCreateAnimator(RevealValues data) {
final Animator animator = createAnimator(data);

animator.addListener(new AnimatorListenerAdapter() {
@Override public void onAnimationStart(Animator animation) {
RevealValues values = getValues(animation);
values.clip(true);
}
// Before animation is started keep them
targets.put(data.target(), data);
animators.put(animator, data);
return animator;
}

@Override public void onAnimationEnd(Animator animation) {
RevealValues values = getValues(animation);
values.clip(false);
targets.remove(values.target());
}
});
/**
* Create custom animator of circular reveal
*
* @param data RevealValues contains information of starting & ending points, animation target and
* current animation values
* @return Animator to manage reveal animation
*/
protected Animator createAnimator(RevealValues data) {
final ObjectAnimator animator =
ObjectAnimator.ofFloat(data, REVEAL, data.startRadius, data.endRadius);

targets.put(data.target(), data);
animator.addListener(getAnimatorCallback());
return animator;
}

private static RevealValues getValues(Animator animator) {
return (RevealValues) ((ObjectAnimator) animator).getTarget();
protected final AnimatorListenerAdapter getAnimatorCallback() {
return animatorCallback;
}

/**
* @return Retruns Animator
*/
protected final RevealValues getValues(Animator animator) {
return animators.get(animator);
}

/**
* @return Map of started animators
*/
public final Map<View, RevealValues> getTargets() {
return targets;
protected final RevealValues getValues(View view) {
return targets.get(view);
}

/**
* @return True if you don't want use Android native reveal animator
* in order to use your own custom one
* @return True if you don't want use Android native reveal animator in order to use your own
* custom one
*/
protected boolean hasCustomerRevealAnimator() {
protected boolean overrideNativeAnimator() {
return false;
}

/**
* @return True if animation was started and it is still running,
* otherwise returns False
* @return True if animation was started and it is still running, otherwise returns False
*/
public boolean isClipped(View child) {
RevealValues data = targets.get(child);
final RevealValues data = getValues(child);
return data != null && data.isClipping();
}

/**
* Applies path clipping on a canvas before drawing child,
* you should save canvas state before transformation and
* you should save canvas state before viewTransformation and
* restore it afterwards
*
* @param canvas Canvas to apply clipping before drawing
* @param child Reveal animation target
* @return True if transformation was successfully applied on
* referenced child, otherwise child be not the target and
* therefore animation was skipped
* @return True if viewTransformation was successfully applied on referenced child, otherwise
* child be not the target and therefore animation was skipped
*/
public boolean transform(Canvas canvas, View child) {
public final boolean transform(Canvas canvas, View child) {
final RevealValues revealData = targets.get(child);
return revealData != null && revealData.applyTransformation(canvas, child);

// Target doesn't has animation values
if (revealData == null) {
return false;
}
// Check whether target consistency
else if (revealData.target != child) {
throw new IllegalStateException("Inconsistency detected, contains incorrect target view");
}
// View doesn't wants to be clipped therefore transformation is useless
else if (!revealData.clipping) {
return false;
}

return viewTransformation.transform(canvas, child, revealData);
}

public static final class RevealValues {
Expand All @@ -112,12 +165,6 @@ public static final class RevealValues {
// Animation target
View target;

// Android Canvas is tricky, we cannot clip circles directly with Canvas API
// but it is allowed using Path, therefore we use it :|
Path path = new Path();

Region.Op op = Region.Op.REPLACE;

public RevealValues(View target, int centerX, int centerY, float startRadius, float endRadius) {
this.target = target;
this.centerX = centerX;
Expand Down Expand Up @@ -148,6 +195,31 @@ public void clip(boolean clipping) {
public boolean isClipping() {
return clipping;
}
}

/**
* Custom View viewTransformation extension used for applying different reveal
* techniques
*/
interface ViewTransformation {

/**
* Apply view viewTransformation
*
* @param canvas Main canvas
* @param child Target to be clipped & revealed
* @return True if viewTransformation is applied, otherwise return fAlse
*/
boolean transform(Canvas canvas, View child, RevealValues values);
}

public static class PathTransformation implements ViewTransformation {

// Android Canvas is tricky, we cannot clip circles directly with Canvas API
// but it is allowed using Path, therefore we use it :|
private final Path path = new Path();

private Region.Op op = Region.Op.REPLACE;

/** @see Canvas#clipPath(Path, Region.Op) */
public Region.Op op() {
Expand All @@ -159,31 +231,18 @@ public void op(Region.Op op) {
this.op = op;
}

/**
* Applies path clipping on a canvas before drawing child,
* you should save canvas state before transformation and
* restore it afterwards
*
* @param canvas Canvas to apply clipping before drawing
* @param child Reveal animation target
* @return True if transformation was successfully applied on
* referenced child, otherwise child be not the target and
* therefore animation was skipped
*/
boolean applyTransformation(Canvas canvas, View child) {
if (child != target || !clipping) {
return false;
}

@Override public boolean transform(Canvas canvas, View child, RevealValues values) {
path.reset();
// trick to applyTransformation animation, when even x & y translations are running
path.addCircle(child.getX() + centerX, child.getY() + centerY, radius, Path.Direction.CW);
path.addCircle(child.getX() + values.centerX, child.getY() + values.centerY, values.radius,
Path.Direction.CW);

canvas.clipPath(path, op);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
child.invalidateOutline();
}
return true;
return false;
}
}

Expand All @@ -199,8 +258,8 @@ private static final class ClipRadiusProperty extends Property<RevealValues, Flo
}

@Override public void set(RevealValues data, Float value) {
data.radius(value);
data.target().invalidate();
data.radius = value;
data.target.invalidate();
}

@Override public Float get(RevealValues v) {
Expand Down
Expand Up @@ -21,20 +21,28 @@ public RevealFrameLayout(Context context, AttributeSet attrs) {

public RevealFrameLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);

manager = new ViewRevealManager();
}

@Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
try {
canvas.save();

manager.transform(canvas, child);
return super.drawChild(canvas, child, drawingTime);
return manager.transform(canvas, child)
& super.drawChild(canvas, child, drawingTime);
} finally {
canvas.restore();
}
}

public void setViewRevealManager(ViewRevealManager manager) {
if (manager == null) {
throw new NullPointerException("ViewRevealManager is null");
}

this.manager = manager;
}

@Override public ViewRevealManager getViewRevealManager() {
return manager;
}
Expand Down
Expand Up @@ -35,6 +35,14 @@ public RevealLinearLayout(Context context, AttributeSet attrs, int defStyle) {
}
}

public void setViewRevealManager(ViewRevealManager manager) {
if (manager == null) {
throw new NullPointerException("ViewRevealManager is null");
}

this.manager = manager;
}

@Override public ViewRevealManager getViewRevealManager() {
return manager;
}
Expand Down

0 comments on commit 7505025

Please sign in to comment.