仿搜狐视频电影海报Gallery效果
搜狐视频的pad版的电影频道(或者电视剧频道)中,海报的切换是幻灯片的方式,当左右滑动选择到一张新的海报时,这张海报会逐渐变大,其实貌似ios的pad版appstore上就有这种效果。个人比较认可这种效果,相对于安卓的Gallery控件(已经不推荐使用)以及后来的ViewPager,横向滚动的ListView,以及网上用RecyclerView实现的各种所谓Gallery,搜狐视频的这个Gallery要有意味的多。
实际上搜狐视频的Gallery并未达到完美模仿appstore的水平,不过已经很接近了。
今天我们就来实现搜狐视频的这种效果。不过,我们也并没有完全实现搜狐视频海报Gallery的所有功能,比如自动播放幻灯片以及循环播放就没有去实现。
思路与方案的选择
在码代码之前,我们需要找到实现的思路。我想到了好几种:自定义ViewPager,修改开源项目CoverFlow ,使用RecyclerView,自定义Gallery控件。其实开源项目CoverFlow 本来就是一个画廊控件,但是其效果是3d倒影效果,滑动的自然程度也不是很理想。我最先尝试了ViewPager的方案,以为最简单,试着做了之后才发现不太好办,因为ViewPager一次只显示一个页面,虽然有一次能显示三页的开源项目,但是最多也只能显示3个。就剩下3种了,一时不太好选择,于是干脆看看搜狐是咋实现的。
反编译
======
我反编译了搜狐视频的源码,虽然搜狐视频并不一定就是用的Gallery控件,但是不管怎样,跟海报效果相关的类了应该也是带了Gallery这个单词的,果不其然,我在反编译的源码中找到了一个GalleryView类。打开一看,其实就是Gallery控件的一个子类。
下面是其经过反编译后的代码:
package com.sohu.sohuvideo.ui.view;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.IntentFilter;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.Gallery;
public class GalleryView extends Gallery
{
private static final int DEFAULT_INTERVAL = 3000;
private static final String TAG = "GalleryView";
private final int FLIP_MSG = 1;
private boolean mAutoStart = false;
private int mFlipInterval = 3000;
private final Handler mHandler = new v(this);
private final BroadcastReceiver mReceiver = new u(this);
private boolean mRunning = false;
private boolean mStarted = false;
private boolean mTouching = false;
private boolean mUserPresent = true;
private boolean mVisible = false;
public GalleryView(Context paramContext)
{
super(paramContext);
}
public GalleryView(Context paramContext, AttributeSet paramAttributeSet)
{
super(paramContext, paramAttributeSet);
}
private void showNext()
{
onScroll(null, null, 1.0F, 0.0F);
onKeyDown(22, null);
}
private void updateRunning()
{
updateRunning(true);
}
private void updateRunning(boolean paramBoolean)
{
boolean bool;
if ((this.mVisible) && (this.mStarted) && (!this.mTouching) && (this.mUserPresent))
{
bool = true;
if (bool != this.mRunning)
{
if (!bool)
break label76;
Message localMessage = this.mHandler.obtainMessage(1);
this.mHandler.sendMessageDelayed(localMessage, this.mFlipInterval);
}
}
while (true)
{
this.mRunning = bool;
return;
bool = false;
break;
label76: this.mHandler.removeMessages(1);
}
}
public boolean dispatchTouchEvent(MotionEvent paramMotionEvent)
{
if (paramMotionEvent.getAction() == 0)
if (this.mRunning)
{
this.mTouching = true;
updateRunning(true);
}
while (true)
{
return super.dispatchTouchEvent(paramMotionEvent);
if ((paramMotionEvent.getAction() == 2) || (!this.mTouching))
continue;
this.mTouching = false;
updateRunning(true);
}
}
public boolean isAutoStart()
{
return this.mAutoStart;
}
public boolean isFlipping()
{
return this.mStarted;
}
protected void onAttachedToWindow()
{
super.onAttachedToWindow();
IntentFilter localIntentFilter = new IntentFilter();
localIntentFilter.addAction("android.intent.action.SCREEN_OFF");
localIntentFilter.addAction("android.intent.action.USER_PRESENT");
getContext().registerReceiver(this.mReceiver, localIntentFilter);
if (this.mAutoStart)
startFlipping();
}
protected void onDetachedFromWindow()
{
super.onDetachedFromWindow();
this.mVisible = false;
getContext().unregisterReceiver(this.mReceiver);
updateRunning(true);
}
public boolean onFling(MotionEvent paramMotionEvent1, MotionEvent paramMotionEvent2, float paramFloat1, float paramFloat2)
{
if (paramMotionEvent1.getX() - paramMotionEvent2.getX() < 0.0F);
for (int i = 21; ; i = 22)
{
onKeyDown(i, null);
return true;
}
}
protected void onWindowVisibilityChanged(int paramInt)
{
super.onWindowVisibilityChanged(paramInt);
if (paramInt == 0);
for (boolean bool = true; ; bool = false)
{
this.mVisible = bool;
updateRunning(false);
return;
}
}
public void setAutoStart(boolean paramBoolean)
{
this.mAutoStart = paramBoolean;
}
public void setFlipInterval(int paramInt)
{
this.mFlipInterval = paramInt;
}
public void startFlipping()
{
this.mStarted = true;
updateRunning(true);
}
public void stopFlipping()
{
this.mStarted = false;
updateRunning(true);
}
}
代码不长,虽然不知道到底做了些什么,但是至少我们已经知道他是用Gallery控件做的。
于是,我也就用Gallery控件做了,从搜狐的代码中,可以看到这里最重要的是重写了onFling方法。
先看看Gallery控件这个方法原本的代码:
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
if (!mShouldCallbackDuringFling) {
// We want to suppress selection changes
// Remove any future code to set mSuppressSelectionChanged = false
removeCallbacks(mDisableSuppressSelectionChangedRunnable);
// This will get reset once we scroll into slots
if (!mSuppressSelectionChanged) mSuppressSelectionChanged = true;
}
// Fling the gallery!
mFlingRunnable.startUsingVelocity((int) -velocityX);
return true;
}
再看看搜狐视频GalleryView中重写了的onFling方法
public boolean onFling(MotionEvent paramMotionEvent1, MotionEvent paramMotionEvent2, float paramFloat1, float paramFloat2)
{
if (paramMotionEvent1.getX() - paramMotionEvent2.getX() < 0.0F);
for (int i = 21; ; i = 22)
{
onKeyDown(i, null);
return true;
}
}
可以看到,GalleryView中,mFlingRunnable.startUsingVelocity((int) -velocityX);没有被调用。而是根据用户的滑动方向主动调用Gallery切换到另一个元素(前一个或者后一个)。然后直接返回,这也就是为什么搜狐视频的Gallery不能一次滑动多张图片的缘故(其实稍微滑动长点还是能滑动几张,但是也没几张,跟默认的比起来少了很多)。第一行显然是判断滑动方向,而里面的for循环应该是混淆导致的结果,原始代码里应该不是for循环。for循环里onKeyDown(i, null)应该就是对应切换到下一个或者上一个的方法了。至于onKeyDown是不是就是如我们猜测的那样的功能,后面会有解释(onKeyDown其实本应由用户产生的按钮事件回调(实体键的手机上),但这里是主动调用。)。
GalleryView其余的代码主要是处理自动循环播放的问题。
纵观整个GalleryView,并没有找到“当左右滑动选择到一张新的海报时,这张海报会逐渐变大”的相关代码。可见这个自定义的GalleryView仅仅是重新定义了onFling的行为。海报的动画效果是在外部实现的。
那么,为什么需要重新定义onFling的行为呢?因为Gallery控件在滑动之后停下来的瞬间,重新调整位置以确保总是有个页面被选中,而这个过程不是很自然,有种顿了一下的感觉,因此干脆屏蔽掉。
于是我自定义了一个如下的Gallery:
/*
* Copyright (C) 2007 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.example.gallery;
import android.content.Context;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.widget.Gallery;
public class GalleryView extends Gallery {
public GalleryView(Context paramContext){
super(paramContext);
}
public GalleryView(Context paramContext, AttributeSet paramAttributeSet){
super(paramContext, paramAttributeSet);
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
if (e1.getX() - e2.getX() < 0.0F){
onKeyDown(KeyEvent.KEYCODE_DPAD_LEFT,null);
}else{
onKeyDown(KeyEvent.KEYCODE_DPAD_RIGHT,null);
}
return true;
}
}
onKeyDown(KeyEvent.KEYCODE_DPAD_LEFT,null)可以切换到前一个,onKeyDown(KeyEvent.KEYCODE_DPAD_RIGHT,null)可以切换到后一个。
Gallery控件中onKeyDown方法的源码如下
/**
* Handles left, right, and clicking
* @see android.view.View#onKeyDown
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_LEFT:
if (movePrevious()) {
playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT);
return true;
}
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
if (moveNext()) {
playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT);
return true;
}
break;
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_ENTER:
mReceivedInvokeKeyDown = true;
// fallthrough to default handling
}
return super.onKeyDown(keyCode, event);
}
其中movePrevious()和moveNext()切换前一个和后一个的方法。 为什么我们不在onFling中直接调用movePrevious和moveNext呢?因为他们是私有的方法,无法调用,而onKeyDown虽说本是处理按键事件的的,但这里也可以主动调用他。达到move的效果。
刚刚说了海报的动画效果是在外面实现的,其实具体来说就是在activity中实现的,搜狐视频的反编译代码中我没有找到,只能靠自己了。
其实很简单,就是在Gallery的OnItemSelectedListener中的OnItemSelected回调方法里面,针对当前的item播放属性动画,需要同时播放两个属性动画(scaleY 和 scaleX)才能达到整体变大的效果,其实这招我也是跟 Miroslaw Stanek 学的,他在 InstaMaterial 概念设计的系列文章 中用了很多属性动画来实现很炫的效果,我直接把他的代码copy过来改改参数就可以了。 虽然我早就会使用属性动画,但是并不知道什么样的搭能实现什么样的效果,从Miroslaw Stanek的文章中,我找到了很多实用的方法。
仅仅在有新的item被选中的时候让当前item变大还不行,你还得让那些不再被选中的item变回原来的大小。这也使用属性动画来实现,不过为了达到更好的效果,这次属性动画的持续时间要小点才好。
好了,可以开始写代码了。
开始实现
=======
现在差不多可以开始写代码了:
xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#333333"
>
<com.example.gallery.GalleryView android:id="@+id/gallery"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:spacing="5dip"
android:unselectedAlpha="50"
/>
</RelativeLayout>
这里添加了一个上面我们定义的GalleryView控件,而非原生的Gallery控件。
activity中:
package com.example.gallery;
import android.support.v7.app.ActionBarActivity;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.Gallery;
import android.widget.ImageView;
public class GalleryActivity extends ActionBarActivity {
private int\[\] myImageIds = {R.drawable.photo1,
R.drawable.photo2,
R.drawable.photo3,
R.drawable.photo4,
R.drawable.photo5,
R.drawable.photo6,R.drawable.photo7};
int lastSelectedPosition= -1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_gallery);
Gallery gallery= (Gallery) findViewById(R.id.gallery);
ImageAdapter imageAdapter = new ImageAdapter(this);
gallery.setAdapter(imageAdapter);
gallery.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@SuppressLint("NewApi")
@Override
public void onItemSelected(AdapterView<?> parent, View v,int position, long id) {
AnimatorSet animatorSet = new AnimatorSet();
ObjectAnimator imgScaleUpYAnim = ObjectAnimator.ofFloat(v, "scaleY", 0.7f, 1f);
imgScaleUpYAnim.setDuration(600);
//imgScaleUpYAnim.setInterpolator(DECCELERATE_INTERPOLATOR);
ObjectAnimator imgScaleUpXAnim = ObjectAnimator.ofFloat(v, "scaleX", 0.7f, 1f);
imgScaleUpXAnim.setDuration(600);
animatorSet.playTogether(imgScaleUpYAnim,imgScaleUpXAnim);
animatorSet.start();
for(int i = 0;i < parent.getChildCount();i++){
if(parent.getChildAt(i) != v){
View s = parent.getChildAt(i);
ObjectAnimator imgScaleDownYAnim = ObjectAnimator.ofFloat(s, "scaleY", 1f, 0.7f);
imgScaleDownYAnim.setDuration(100);
//imgScaleUpYAnim.setInterpolator(DECCELERATE_INTERPOLATOR);
ObjectAnimator imgScaleDownXAnim = ObjectAnimator.ofFloat(s, "scaleX", 1f, 0.7f);
imgScaleDownXAnim.setDuration(100);
animatorSet.playTogether(imgScaleDownXAnim,imgScaleDownYAnim);
animatorSet.start();
}
}
}
@Override
public void onNothingSelected(AdapterView<?> arg0) {
}
});
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.gallery, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
public class ImageAdapter extends BaseAdapter{
private Context mContext;
public ImageAdapter(Context context){
mContext = context;
}
public int getCount(){
return myImageIds.length;
}
public Object getItem(int position){
return position;
}
public long getItemId(int position){
return position;
}
@SuppressLint("NewApi")
public View getView(int position, View convertView, ViewGroup parent){
ImageView imageView = new ImageView(mContext);
imageView.setImageResource(myImageIds\[position\]);
imageView.setScaleType(ImageView.ScaleType.FIT_XY);
imageView.setLayoutParams(new Gallery.LayoutParams(Gallery.LayoutParams.WRAP_CONTENT, Gallery.LayoutParams.WRAP_CONTENT));
imageView.setScaleX(0.7f);
imageView.setScaleY(0.7f);
return imageView;
}
}
}
其中当item未被选中时,其缩放比例只有0.7,被选中的时候才回到原始比例。其实经过验证,如果被选中的时候,item是原始大小的1.1背效果更加,因为这样被选中的item(本例中是图片)会稍微超出控件的边界,你可以根据自己的喜好,自己设置,同样item未被选中时,你也可以设置缩放比例为0.6或者0.8.
为了那些不再被选中的item变回原来的大小,我们使用for循环来遍历Gallery的所有子控件,然后每个子控件都缩回0.7的比例
for(int i = 0;i < parent.getChildCount();i++){
if(parent.getChildAt(i) != v){
View s = parent.getChildAt(i);
ObjectAnimator imgScaleDownYAnim = ObjectAnimator.ofFloat(s, "scaleY", 1f, 0.7f);
imgScaleDownYAnim.setDuration(100);
//imgScaleUpYAnim.setInterpolator(DECCELERATE_INTERPOLATOR);
ObjectAnimator imgScaleDownXAnim = ObjectAnimator.ofFloat(s, "scaleX", 1f, 0.7f);
imgScaleDownXAnim.setDuration(100);
animatorSet.playTogether(imgScaleDownXAnim,imgScaleDownYAnim);
animatorSet.start();
}
}
这个地方的实现有点差强人意,在以后的更新中我会找到更妥当的方式。
因为demo中只有几张图片,Gallery的adapter的getView我们没有使用ViewHolder来减少对象的创建,同时注意,在getView中我们也将imageView的缩放大小设置成了0.7,这应该很好理解。
好了现在上效果图: