注:
本文及以后设计的官方Examples 均来自QT4.8.7。
文章不涉及QT C++基础语法,错误之处望各位大佬指正。
Animated Tiles是 Animation Framework的第一个例子,废话不多说,直接看演示
这样的动画效果是如何做到的呢,在源码中我们可以看到,这个例子主要用到了两个知识点,一个是QState,也就是状态机,另一个是QGraphicsWidget ,QWidget我在项目中用的多了,但QGraphicsWidget着实没用到几次,QGraphicsWidget和QWidget应该来说分属于两个不同的框架,QGraphicsWidget继承于QGraphicsItem,在QGraphicsItem的基础上添加了Widget。
理解这个例子只要理解QGraphicsWidget 的绘图原理就行了,
我们按照官方例子看一下QGraphicsWidget的绘图步骤
QGraphicsScene scene(-350, -350, 700, 700);
QGraphicsItem *buttonParent = new QGraphicsRectItem;
Button *ellipseButton = new Button(QPixmap(":/images/ellipse.png"), buttonParent);
scene.addItem(buttonParent);
buttonParent->scale(0.75, 0.75);
buttonParent->setPos(200, 200);
buttonParent->setZValue(65);
View *view = new View(&scene);
view->setWindowTitle(QT_TRANSLATE_NOOP(QGraphicsView, "Animated Tiles"));
view->setViewportUpdateMode(QGraphicsView::BoundingRectViewportUpdate);
view->setBackgroundBrush(bgPix);
view->setCacheMode(QGraphicsView::CacheBackground);
view->setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
我们可以比较形象的理解为QGraphicsScene-> QGraphicsView->QGraphicsWidget->QGraphicsItem.
着重要指出的是,QGraphics中各个类的坐标系其实是不一样的:
1.QGraphicsItem坐标:属于局部坐标,通常以QGraphicsItem中心为原点(中心对称),正方向x朝右,y朝下。
2.setPos的坐标是父类坐标系的坐标,一般对于QGraphicsItem位于QGraphicsScene中的应用场景。
3.QGraphicsScene坐标:属于逻辑坐标 logical coordinates(与QPainter相同),以场景中心为原点,正方向x朝右,y朝下。
4.图元原点(左上角dialog的原点)与场景原点对齐,导致图元外边框的左上角顶点在场景中的坐标位置为(负数,负数)。
5.View(视图)坐标:属于设备坐标device coordinates(与窗口相同),默认以左上点为原点, 正方向x朝右,y朝下。
6.默认场景scene的左上角顶点与视图坐标原点对齐。显示时默认中心对齐,当场景大小小于视图大小的时候,将中心对齐,此中指的仍然是整个图元的中心,同时,图元原点与场景原点对齐,场景左上角顶点与视图原点对齐,视图左上角顶点不一定是场景原点,此时也将出现视图坐标有正值有负值。
详细QGraphics的应用这里就不再赘述了。
QState 比较好理解,
// States
QState *rootState = new QState;
QState *ellipseState = new QState(rootState);
QState *figure8State = new QState(rootState);
QState *randomState = new QState(rootState);
QState *tiledState = new QState(rootState);
QState *centeredState = new QState(rootState);
//创建5个状态,依次是圆形状态,figure8状态 随机状态 平铺状态 中心状态
//这5个状态分别表示64个item的排列状态,具体排列位置看代码
// Values
for (int i = 0; i < items.count(); ++i) {
Pixmap *item = items.at(i);
// Ellipse
ellipseState->assignProperty(item, "pos",
QPointF(cos((i / 63.0) * 6.28) * 250,
sin((i / 63.0) * 6.28) * 250));
// Figure 8
figure8State->assignProperty(item, "pos",
QPointF(sin((i / 63.0) * 6.28) * 250,
sin(((i * 2)/63.0) * 6.28) * 250));
// Random
randomState->assignProperty(item, "pos",
QPointF(-250 + qrand() % 500,
-250 + qrand() % 500));
// Tiled
tiledState->assignProperty(item, "pos",
QPointF(((i % 8) - 4) * kineticPix.width() + kineticPix.width() / 2,
((i / 8) - 4) * kineticPix.height() + kineticPix.height() / 2));
// Centered
centeredState->assignProperty(item, "pos", QPointF());
}
//创建状态机
QStateMachine states;
//初始化状态机
states.addState(rootState);
states.setInitialState(rootState);
//初始化状态设置为centeredState
rootState->setInitialState(centeredState);
//下面是状态机触发的信号和槽 QParallelAnimationGroup 是动画容器,不会影响item的最终位置
//其实我们可以只需要rootState->addTransition(ellipseButton, SIGNAL(pressed()), ellipseState);添加状态机转换的信号和槽就可以
QParallelAnimationGroup *group = new QParallelAnimationGroup;
QAbstractTransition *trans = rootState->addTransition(ellipseButton, SIGNAL(pressed()), ellipseState);
trans->addAnimation(group);
trans = rootState->addTransition(figure8Button, SIGNAL(pressed()), figure8State);
trans->addAnimation(group);
trans = rootState->addTransition(randomButton, SIGNAL(pressed()), randomState);
trans->addAnimation(group);
trans = rootState->addTransition(tiledButton, SIGNAL(pressed()), tiledState);
trans->addAnimation(group);
trans = rootState->addTransition(centeredButton, SIGNAL(pressed()), centeredState);
trans->addAnimation(group);
上面的代码阐述的是状态机的添加流程,以及如何设置状态机,要使状态机真正生效,别忘了states.start();
状态机的引入,并不是只能改变控件的空间位置,最主要的还是能改变窗口控件的其他属性:比如visible ,text,checked,icon等 避免频繁使用控件内置函数,增加不必要的判断,就像QT描述的那样 Code less ,Create more
真正实现动画效果的,其实是QPropertyAnimation QParallelAnimationGroup
来看一下他们的基本关系
QParallelAnimationGroup 是一个并行动画组类
通过例子中的代码
QParallelAnimationGroup *group = new QParallelAnimationGroup;
for (int i = 0; i < items.count(); ++i) {
QPropertyAnimation *anim = new QPropertyAnimation(items[i], "pos");
anim->setDuration(750 + i * 25);
anim->setEasingCurve(QEasingCurve::InOutBack);
group->addAnimation(anim);
}
QAbstractTransition *trans = rootState->addTransition(ellipseButton, SIGNAL(pressed()), ellipseState);
trans->addAnimation(group);
上面的代码清晰的阐释了例子动画效果的原理
QPropertyAnimation 是动画效果属性的基类, anim->setDuration 的意思是 动作时长
anim->setEasingCurve(QEasingCurve::InOutBack); 设置动画效果 大致就是设置动画的缓和曲线
差不多大家也都知道了,按照代码所写的,每一个图元的飞行时间不一致,随item的id增加而飞行时间增加,这就形成了例子演示中,item依次飞行的现象,
item的层数其实也有差异,这就要涉及到在创建item时的setZValue值,value值越大,图层在越上层,图元层叠的效果就是这么来的
最后再看看Button是怎么画出来的吧
class Button : public QGraphicsWidget
{
Q_OBJECT
public:
Button(const QPixmap &pixmap, QGraphicsItem *parent = 0)
: QGraphicsWidget(parent), _pix(pixmap)
{
//接收悬停事件
setAcceptHoverEvents(true);
setCacheMode(DeviceCoordinateCache);
}
QRectF boundingRect() const
{
return QRectF(-65, -65, 130, 130);
}
QPainterPath shape() const
{
QPainterPath path;
path.addEllipse(boundingRect());
return path;
}
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *)
{
//重绘时鼠标是否按下
bool down = option->state & QStyle::State_Sunken;
QRectF r = boundingRect();
//线性渐变区域
QLinearGradient grad(r.topLeft(), r.bottomRight());
//设置梯度颜色
grad.setColorAt(down ? 1 : 0, option->state & QStyle::State_MouseOver ? Qt::white : Qt::lightGray);
grad.setColorAt(down ? 0 : 1, Qt::darkGray);
painter->setPen(Qt::darkGray);
painter->setBrush(grad);
//bounding区域画圆
painter->drawEllipse(r);
//画一个小圆并填充图片
QLinearGradient grad2(r.topLeft(), r.bottomRight());
grad.setColorAt(down ? 1 : 0, Qt::darkGray);
grad.setColorAt(down ? 0 : 1, Qt::lightGray);
painter->setPen(Qt::NoPen);
painter->setBrush(grad);
if (down)
painter->translate(2, 2);
painter->drawEllipse(r.adjusted(5, 5, -5, -5));
painter->drawPixmap(-_pix.width()/2, -_pix.height()/2, _pix);
}
signals:
void pressed();
protected:
void mousePressEvent(QGraphicsSceneMouseEvent *)
{
emit pressed();
update();
}
void mouseReleaseEvent(QGraphicsSceneMouseEvent *)
{
update();
}
private:
QPixmap _pix;
};
代码里面已经加了注释,差不多应该能看懂吧