/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.chartlib;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.awt.geom.Path2D;
import java.util.LinkedList;
import java.util.List;
import javax.swing.JComponent;
import javax.swing.Timer;
/**
* Base class for components that should change their look over time.
*
* At a minimum, child classes should override {@link #updateData()} and {@link #draw(Graphics2D)},
* as well as pay attention to the field {@link #mFrameLength} as it controls the behavior of timed
* animations.
*/
public abstract class AnimatedComponent extends JComponent
implements ActionListener, HierarchyListener {
protected static final Font DEFAULT_FONT = new Font("Sans", Font.PLAIN, 10);
protected final Timer mTimer;
/**
* The length of the last frame in seconds.
*/
protected float mFrameLength;
protected long mLastRenderTime;
protected boolean mDrawDebugInfo;
protected boolean mUpdateData;
protected boolean mStep;
private List<String> mDebugInfo;
public AnimatedComponent(int fps) {
mUpdateData = true;
mTimer = new Timer(1000 / fps, this);
mDebugInfo = new LinkedList<String>();
addHierarchyListener(this);
}
/**
* A linear interpolation that accumulates over time. This gives an exponential effect where the
* value {@code from} moves towards the value {@code to} at a rate of {@code fraction} per
* second. The actual interpolated amount depends on the current frame length.
*
* @param from the value to interpolate from.
* @param to the target value.
* @param fraction the interpolation fraction.
* @return the interpolated value.
*/
protected final float lerp(float from, float to, float fraction) {
float q = (float) Math.pow(1.0f - fraction, mFrameLength);
return from * q + to * (1.0f - q);
}
public final boolean isDrawDebugInfo() {
return mDrawDebugInfo;
}
public final void setDrawDebugInfo(boolean drawDebugInfo) {
mDrawDebugInfo = drawDebugInfo;
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
// Update frame length.
long now = System.nanoTime();
mFrameLength = (now - mLastRenderTime) / 1000000000.0f;
mLastRenderTime = now;
if (mUpdateData || mStep) {
mStep = false;
updateData();
}
draw(g2d);
if (mDrawDebugInfo) {
doDebugDraw(g2d);
}
g2d.dispose();
}
protected final void addDebugInfo(String format, Object... values) {
if (mDrawDebugInfo) {
mDebugInfo.add(String.format(format, values));
}
}
private void doDebugDraw(Graphics2D g) {
debugDraw(g);
addDebugInfo("Render time: %.2fms", (System.nanoTime() - mLastRenderTime) / 1000000.f);
addDebugInfo("FPS: %.2f", (1.0f / mFrameLength));
g.setFont(DEFAULT_FONT);
g.setColor(Color.BLACK);
int i = 0;
for (String s : mDebugInfo) {
g.drawString(s, getSize().width - 150, getSize().height - 10 * i++ - 5);
}
mDebugInfo.clear();
}
/**
* First step of the animation, this is where the data is read and the current animation values
* are fixed.
*/
protected abstract void updateData();
/**
* Renders the data constructed in the update phase to the given graphics context.
*/
protected abstract void draw(Graphics2D g);
/**
* Draws visual debug information.
*/
protected void debugDraw(Graphics2D g) {
}
@Override
public final void actionPerformed(ActionEvent actionEvent) {
repaint();
}
@Override
public final void hierarchyChanged(HierarchyEvent hierarchyEvent) {
if (mTimer.isRunning() && !isShowing()) {
mTimer.stop();
} else if (!mTimer.isRunning() && isShowing()) {
mTimer.start();
}
}
/**
* If true, this component will animate normally.
*/
public final void setUpdateData(boolean updateData) {
mUpdateData = updateData;
}
/**
* Animate this component for a single frame and then pause. Note that this call will have no
* effect unless {@link #setUpdateData(boolean)} is set to {@code false} first.
*/
public final void step() {
mStep = true;
}
protected static void drawArrow(Graphics2D g, float x, float y, float dx, float dy, float len,
Color color) {
Path2D.Float path = new Path2D.Float();
path.moveTo(x, y);
path.lineTo(x + dx * len, y + dy * len);
path.lineTo(x + dx * (len - 10) + dy * 10, y + dy * (len - 10) - dx * 10);
path.lineTo(x + dx * (len - 10) - dy * 10, y + dy * (len - 10) + dx * 10);
g.setColor(color);
g.draw(path);
}
protected static void drawMarker(Graphics2D g, float x, float y, Color color) {
Path2D.Float path = new Path2D.Float();
path.moveTo(x - 10, y);
path.lineTo(x + 10, y);
path.moveTo(x, y - 10);
path.lineTo(x, y + 10);
g.setColor(color);
g.draw(path);
}
}