android中的左右滑动

标签: android 左右 | 发表时间:2011-06-23 13:47 | 作者:(author unknown) Pei
出处:http://www.iteye.com
iphone中有很多应用都能够左右滑动,非常cool,关键是实现起来非常简单。android比起来就差远了,网上有不少帖子。 我在这边重新分享下自己的经验吧,将实现细节详细解释下。





FlingGallery这个类摘自网上,有少许修改。
package com.nuomi.ui;

import java.util.HashSet;
import java.util.Set;

import android.content.Context;
import android.view.GestureDetector;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import android.view.animation.Transformation;
import android.widget.Adapter;
import android.widget.FrameLayout;
import android.widget.LinearLayout;


public class FlingGallery extends FrameLayout
{
	
	private Set<OnGalleryChangeListener> listeners;
	private final int swipe_min_distance = 120;
    private final int swipe_max_off_path = 250;
    private final int swipe_threshold_veloicty = 400;

	private int mViewPaddingWidth = 0;
    private int mAnimationDuration = 250;
    private float mSnapBorderRatio = 0.5f;
    private boolean mIsGalleryCircular = true;

    private int mGalleryWidth = 0;
    private boolean mIsTouched = false;
    private boolean mIsDragging = false;
    private float mCurrentOffset = 0.0f;
    private long mScrollTimestamp = 0;
    private int mFlingDirection = 0;
    private int mCurrentPosition = 0;
    private int mCurrentViewNumber = 0;

    private Context mContext;
    private Adapter mAdapter;
    private FlingGalleryView[] mViews;
    private FlingGalleryAnimation mAnimation;
    private GestureDetector mGestureDetector;
    private Interpolator mDecelerateInterpolater;

    public FlingGallery(Context context)
	{
		super(context);

		listeners = new HashSet<OnGalleryChangeListener>();
		
		mContext = context;
		mAdapter = null;
		
        mViews = new FlingGalleryView[3];
        mViews[0] = new FlingGalleryView(0, this);
        mViews[1] = new FlingGalleryView(1, this);
        mViews[2] = new FlingGalleryView(2, this);

		mAnimation = new FlingGalleryAnimation();
		mGestureDetector = new GestureDetector(new FlingGestureDetector());
		mDecelerateInterpolater = AnimationUtils.loadInterpolator(mContext, android.R.anim.decelerate_interpolator);
	}

    public void addGalleryChangeListener(OnGalleryChangeListener listener){
    	listeners.add(listener);
    }
    
	public void setPaddingWidth(int viewPaddingWidth)
	{
		mViewPaddingWidth = viewPaddingWidth;
	}

	public void setAnimationDuration(int animationDuration)
	{
		mAnimationDuration = animationDuration;
	}
	
	public void setSnapBorderRatio(float snapBorderRatio)
	{
		mSnapBorderRatio = snapBorderRatio;
	}

	public void setIsGalleryCircular(boolean isGalleryCircular) 
	{
		if (mIsGalleryCircular != isGalleryCircular)
		{
			mIsGalleryCircular = isGalleryCircular;
	
			if (mCurrentPosition == getFirstPosition())
			{
				// We need to reload the view immediately to the left to change it to circular view or blank
		    	mViews[getPrevViewNumber(mCurrentViewNumber)].recycleView(getPrevPosition(mCurrentPosition));			
			}
	
			if (mCurrentPosition == getLastPosition())
			{
				// We need to reload the view immediately to the right to change it to circular view or blank
		    	mViews[getNextViewNumber(mCurrentViewNumber)].recycleView(getNextPosition(mCurrentPosition));			
			}
		}
	}

	public int getGalleryCount()
	{
		return (mAdapter == null) ? 0 : mAdapter.getCount();
	}

	public int getFirstPosition()
	{
		return 0;
	}

	public int getLastPosition()
	{
		return (getGalleryCount() == 0) ? 0 : getGalleryCount() - 1;
	}

	private int getPrevPosition(int relativePosition)
	{
		int prevPosition = relativePosition - 1;

		if (prevPosition < getFirstPosition())
		{
			prevPosition = getFirstPosition() - 1;

			if (mIsGalleryCircular == true)
			{
				prevPosition = getLastPosition();
			}
		}
		NotifyGalleryChange();
		return prevPosition;
	}

	private int getNextPosition(int relativePosition)
	{
		int nextPosition = relativePosition + 1;

		if (nextPosition > getLastPosition())
		{
			nextPosition = getLastPosition() + 1;

			if (mIsGalleryCircular == true)
			{
				nextPosition = getFirstPosition();
			}
		}
		NotifyGalleryChange();
		return nextPosition;
	}

	//
	private void NotifyGalleryChange() {
		for (OnGalleryChangeListener listener :listeners) {
			listener.onGalleryChange(mCurrentPosition);
		}
	}

	private int getPrevViewNumber(int relativeViewNumber)
	{
		return (relativeViewNumber == 0) ? 2 : relativeViewNumber - 1;
	}

	private int getNextViewNumber(int relativeViewNumber)
	{
		return (relativeViewNumber == 2) ? 0 : relativeViewNumber + 1;
	}
	
	@Override
	protected void onLayout(boolean changed, int left, int top, int right, int bottom)
	{
		super.onLayout(changed, left, top, right, bottom);

		// Calculate our view width
		mGalleryWidth = right - left;

		if (changed)
		{
	    	// Position views at correct starting offsets
	    	mViews[0].setOffset(0, 0, mCurrentViewNumber);
	    	mViews[1].setOffset(0, 0, mCurrentViewNumber);
	    	mViews[2].setOffset(0, 0, mCurrentViewNumber);
	    }
	}

	public void setAdapter(Adapter adapter)
    {
    	mAdapter = adapter;
    	mCurrentPosition = 0;
        mCurrentViewNumber = 0;

        // Load the initial views from adapter
        mViews[0].recycleView(mCurrentPosition);
    	mViews[1].recycleView(getNextPosition(mCurrentPosition));
    	mViews[2].recycleView(getPrevPosition(mCurrentPosition));

    	// Position views at correct starting offsets
    	mViews[0].setOffset(0, 0, mCurrentViewNumber);
    	mViews[1].setOffset(0, 0, mCurrentViewNumber);
    	mViews[2].setOffset(0, 0, mCurrentViewNumber);
    }

	private int getViewOffset(int viewNumber, int relativeViewNumber)
	{
		// Determine width including configured padding width
		int offsetWidth = mGalleryWidth + mViewPaddingWidth;

		// Position the previous view one measured width to left
		if (viewNumber == getPrevViewNumber(relativeViewNumber))
		{
			return offsetWidth;
		}

		// Position the next view one measured width to the right
		if (viewNumber == getNextViewNumber(relativeViewNumber))
		{
			return offsetWidth * -1;
		}

		return 0;
	}

	void movePrevious()
	{
		// Slide to previous view
		mFlingDirection = 1;
		processGesture();
	}

	void moveNext()
	{
		// Slide to next view
		mFlingDirection = -1;
		processGesture();
	}

	 @Override
	 public boolean onKeyDown(int keyCode, KeyEvent event)
	 {
	    switch (keyCode)
	    {
	    case KeyEvent.KEYCODE_DPAD_LEFT:
	        movePrevious();
	        return true;
	
	    case KeyEvent.KEYCODE_DPAD_RIGHT:
	        moveNext();
	        return true;
	
	    case KeyEvent.KEYCODE_DPAD_CENTER:
	    case KeyEvent.KEYCODE_ENTER:
	    }

	    return super.onKeyDown(keyCode, event);
	}

	public boolean onGalleryTouchEvent(MotionEvent event)
	{
		boolean consumed = mGestureDetector.onTouchEvent(event);
		
		if (event.getAction() == MotionEvent.ACTION_UP)
		{
			if (mIsTouched || mIsDragging)
			{
				processScrollSnap();
				processGesture();
			}
		}
		
        return consumed;
    }

	void processGesture()
	{
		int newViewNumber = mCurrentViewNumber;
		int reloadViewNumber = 0;
		int reloadPosition = 0;

		mIsTouched = false;
		mIsDragging = false;

		if (mFlingDirection > 0)
		{
			if (mCurrentPosition > getFirstPosition() || mIsGalleryCircular == true)
			{
				// Determine previous view and outgoing view to recycle
				newViewNumber = getPrevViewNumber(mCurrentViewNumber);
				mCurrentPosition = getPrevPosition(mCurrentPosition);
				reloadViewNumber = getNextViewNumber(mCurrentViewNumber); 
				reloadPosition = getPrevPosition(mCurrentPosition);
			}
		}

		if (mFlingDirection < 0)
		{
			if (mCurrentPosition < getLastPosition() || mIsGalleryCircular == true)
			{
				// Determine the next view and outgoing view to recycle
				newViewNumber = getNextViewNumber(mCurrentViewNumber);
				mCurrentPosition = getNextPosition(mCurrentPosition);
				reloadViewNumber = getPrevViewNumber(mCurrentViewNumber);
				reloadPosition = getNextPosition(mCurrentPosition);
			}
		}

		if (newViewNumber != mCurrentViewNumber)
		{
			mCurrentViewNumber = newViewNumber; 

			// Reload outgoing view from adapter in new position
			mViews[reloadViewNumber].recycleView(reloadPosition);
		}

		// Ensure input focus on the current view
		mViews[mCurrentViewNumber].requestFocus();

		// Run the slide animations for view transitions
		mAnimation.prepareAnimation(mCurrentViewNumber);
		this.startAnimation(mAnimation);

		// Reset fling state
		mFlingDirection = 0;
	}

	void processScrollSnap()
	{
		// Snap to next view if scrolled passed snap position
		float rollEdgeWidth = mGalleryWidth * mSnapBorderRatio;
		int rollOffset = mGalleryWidth - (int) rollEdgeWidth;
		int currentOffset = mViews[mCurrentViewNumber].getCurrentOffset();

		if (currentOffset <= rollOffset * -1)
		{
			// Snap to previous view
			mFlingDirection = 1;
		}

		if (currentOffset >= rollOffset)
		{
			// Snap to next view
			mFlingDirection = -1;
		}
	}

	private class FlingGalleryView
	{
		private int mViewNumber;
		private FrameLayout mParentLayout;
		
		private FrameLayout mInvalidLayout = null;
		private LinearLayout mInternalLayout = null;
		private View mExternalView = null;

		public FlingGalleryView(int viewNumber, FrameLayout parentLayout)
		{
			mViewNumber = viewNumber;
			mParentLayout = parentLayout;

			// Invalid layout is used when outside gallery
			mInvalidLayout = new FrameLayout(mContext);
			mInvalidLayout.setLayoutParams(new LinearLayout.LayoutParams( 
	                LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));

			// Internal layout is permanent for duration
			mInternalLayout = new LinearLayout(mContext);
			mInternalLayout.setLayoutParams(new LinearLayout.LayoutParams( 
	                LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));

			mParentLayout.addView(mInternalLayout);
		}

		public void recycleView(int newPosition)
		{
			if (mExternalView != null)
			{
				mInternalLayout.removeView(mExternalView);
			}

			if (mAdapter != null)
			{
				if (newPosition >= getFirstPosition() && newPosition <= getLastPosition())
				{
					mExternalView = mAdapter.getView(newPosition, mExternalView, mInternalLayout);
				}
				else
				{
					mExternalView = mInvalidLayout;
				}
			}

			if (mExternalView != null)
			{
				mInternalLayout.addView(mExternalView, new LinearLayout.LayoutParams( 
	                LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
			}
		}

		public void setOffset(int xOffset, int yOffset, int relativeViewNumber)
		{
			// Scroll the target view relative to its own position relative to currently displayed view
			mInternalLayout.scrollTo(getViewOffset(mViewNumber, relativeViewNumber) + xOffset, yOffset);
		}
		
		public int getCurrentOffset()
		{
			// Return the current scroll position
			return mInternalLayout.getScrollX();
		}

		public void requestFocus()
		{
			mInternalLayout.requestFocus();
		}
	}

    private class FlingGalleryAnimation extends Animation
    {
    	private boolean mIsAnimationInProgres;
    	private int mRelativeViewNumber;
    	private int mInitialOffset;
    	private int mTargetOffset;
    	private int mTargetDistance;   	
 
    	public FlingGalleryAnimation()
    	{
    		mIsAnimationInProgres = false;
    		mRelativeViewNumber = 0;
        	mInitialOffset = 0;
        	mTargetOffset = 0;
        	mTargetDistance = 0;
    	}
 
    	public void prepareAnimation(int relativeViewNumber)
    	{
    		// If we are animating relative to a new view
    		if (mRelativeViewNumber != relativeViewNumber)
    		{
				if (mIsAnimationInProgres == true)
				{
					// We only have three views so if requested again to animate in same direction we must snap 
					int newDirection = (relativeViewNumber == getPrevViewNumber(mRelativeViewNumber)) ? 1 : -1;
	    			int animDirection = (mTargetDistance < 0) ? 1 : -1; 

	    			// If animation in same direction
	    			if (animDirection == newDirection)
	    			{
		        		// Ran out of time to animate so snap to the target offset
		        		mViews[0].setOffset(mTargetOffset, 0, mRelativeViewNumber);
						mViews[1].setOffset(mTargetOffset, 0, mRelativeViewNumber);
						mViews[2].setOffset(mTargetOffset, 0, mRelativeViewNumber);	
	    			}
				}
	
				// Set relative view number for animation
	    		mRelativeViewNumber = relativeViewNumber;
    		}

			// Note: In this implementation the targetOffset will always be zero
    		// as we are centering the view; but we include the calculations of
			// targetOffset and targetDistance for use in future implementations

			mInitialOffset = mViews[mRelativeViewNumber].getCurrentOffset();
			mTargetOffset = getViewOffset(mRelativeViewNumber, mRelativeViewNumber);
			mTargetDistance = mTargetOffset - mInitialOffset;

			// Configure base animation properties
			this.setDuration(mAnimationDuration);
			this.setInterpolator(mDecelerateInterpolater);

			// Start/continued animation
			mIsAnimationInProgres = true;
		}

        @Override
        protected void applyTransformation(float interpolatedTime, Transformation transformation)
        {
        	// Ensure interpolatedTime does not over-shoot then calculate new offset
        	interpolatedTime = (interpolatedTime > 1.0f) ? 1.0f : interpolatedTime;
			int offset = mInitialOffset + (int) (mTargetDistance * interpolatedTime);

			for (int viewNumber = 0; viewNumber < 3; viewNumber++)
			{
				// Only need to animate the visible views as the other view will always be off-screen
				if ((mTargetDistance > 0 && viewNumber != getNextViewNumber(mRelativeViewNumber)) ||
					(mTargetDistance < 0 && viewNumber != getPrevViewNumber(mRelativeViewNumber)))
				{
					mViews[viewNumber].setOffset(offset, 0, mRelativeViewNumber);
				}
			}
        }

        @Override
        public boolean getTransformation(long currentTime, Transformation outTransformation)
        {
        	if (super.getTransformation(currentTime, outTransformation) == false)
        	{
        		// Perform final adjustment to offsets to cleanup animation
        		mViews[0].setOffset(mTargetOffset, 0, mRelativeViewNumber);
				mViews[1].setOffset(mTargetOffset, 0, mRelativeViewNumber);
				mViews[2].setOffset(mTargetOffset, 0, mRelativeViewNumber);

				// Reached the animation target
				mIsAnimationInProgres = false;

				return false;
        	}
 
        	// Cancel if the screen touched
        	if (mIsTouched || mIsDragging)
        	{
        		// Note that at this point we still consider ourselves to be animating
        		// because we have not yet reached the target offset; its just that the
        		// user has temporarily interrupted the animation with a touch gesture

        		return false;
        	}

        	return true;
        }
    }

	private class FlingGestureDetector extends GestureDetector.SimpleOnGestureListener
    {
    	@Override
    	public boolean onDown(MotionEvent e)
    	{
    		// Stop animation
    		mIsTouched = true;

    		// Reset fling state
    		mFlingDirection = 0;
            return true;
    	}

    	@Override
    	public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)
    	{
    		if (e2.getAction() == MotionEvent.ACTION_MOVE)
        	{
	    		if (mIsDragging == false)
	    		{
	        		// Stop animation
	    			mIsTouched = true;
	 
	    			// Reconfigure scroll
	    			mIsDragging = true;
	    			mFlingDirection = 0;
	    			mScrollTimestamp = System.currentTimeMillis();
	    			mCurrentOffset = mViews[mCurrentViewNumber].getCurrentOffset();
	    		}

        	    float maxVelocity = mGalleryWidth / (mAnimationDuration / 1000.0f);
        		long timestampDelta = System.currentTimeMillis() - mScrollTimestamp;
        		float maxScrollDelta = maxVelocity * (timestampDelta / 1000.0f); 
        		float currentScrollDelta = e1.getX() - e2.getX();

        		if (currentScrollDelta < maxScrollDelta * -1) currentScrollDelta = maxScrollDelta * -1;
        		if (currentScrollDelta > maxScrollDelta) currentScrollDelta = maxScrollDelta;
	        	int scrollOffset = Math.round(mCurrentOffset + currentScrollDelta);

        		// We can't scroll more than the width of our own frame layout
        		if (scrollOffset >= mGalleryWidth) scrollOffset = mGalleryWidth;
        		if (scrollOffset <= mGalleryWidth * -1) scrollOffset = mGalleryWidth * -1;
        		
        		mViews[0].setOffset(scrollOffset, 0, mCurrentViewNumber);
    			mViews[1].setOffset(scrollOffset, 0, mCurrentViewNumber);
    			mViews[2].setOffset(scrollOffset, 0, mCurrentViewNumber);
        	}

            return false;
    	}

    	@Override
    	public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)
    	{
            if (Math.abs(e1.getY() - e2.getY()) <= swipe_max_off_path)
            {
                if (e2.getX() - e1.getX() > swipe_min_distance && Math.abs(velocityX) > swipe_threshold_veloicty)
                {
                	movePrevious();
                }

                if(e1.getX() - e2.getX() > swipe_min_distance && Math.abs(velocityX) > swipe_threshold_veloicty)
                {
                	moveNext();
                }
            }

            return false;
    	}

    	@Override
    	public void onLongPress(MotionEvent e)
    	{
    		// Finalise scrolling
    		mFlingDirection = 0;
            processGesture();
    	}

    	@Override
    	public void onShowPress(MotionEvent e)
    	{
    	}

    	@Override
    	public boolean onSingleTapUp(MotionEvent e)
    	{
    		// Reset fling state
    		mFlingDirection = 0;
            return false;
    	}
    	
    }

	public GestureDetector getMGestureDetector() {
		return mGestureDetector;
	}
	
}


由于我需要在滑动页面时,改动title中的文字,这里采用了观察者模式,加了个OnGalleryChangeListener,有同样需求的同学可以参考下。
public interface OnGalleryChangeListener {
	
	public void onGalleryChange(int currentItem);
}

在Activity中,
FlingGallery gallery = new FlingGallery(this);

		gallery.setAdapter(new ArrayAdapter<String>(getApplicationContext(),
				android.R.layout.simple_list_item_1, new String[xxxx]) {
			public View getView(int position, View convertView, ViewGroup parent) {
                               // 返回滑动的deal
				return dealViews[position];
			}
		});
		gallery.addGalleryChangeListener(new OnGalleryChangeListener() {

			@Override
			public void onGalleryChange(int currentItem) {
				// 干些想干的事件

			}

		});

将gallery加到Activity中的最终需要显示的类中。
Adapter中的getView方法中,将需要用来滑动的view添加进来。在GalleryChangeListener中,可以干一些自己想要的滑动后的事情。我在这里改动了标题的文字,进行了我的view中图片的lazyloading。
这里提一下另一个问题。我的这个类,最终嵌入到了tab中,需要在你的tabActivity中dispatchKeyEvent一下,将按键事件分发下去。
最开始,我的滑动的view写得比较通用,所以包含了ScrollView来满足比较长的屏幕,导致手势监听会出一些问题,会出现抖动。当时的解决方案是针对不同屏幕尽量保证一屏能够显示,在res目录下,增加layout-800x480之类的目录,针对每个不同屏幕设计单独的layout,放弃上下滑动,效果也不错。






作者: yf42 
声明: 本文系ITeye网站发布的原创文章,未经作者书面许可,严禁任何网站转载本文,否则必将追究法律责任!

已有 0 人发表回复,猛击->>这里<<-参与讨论


ITeye推荐



相关 [android 左右] 推荐:

android中的左右滑动

- Pei - ITeye论坛最新讨论
iphone中有很多应用都能够左右滑动,非常cool,关键是实现起来非常简单. android比起来就差远了,网上有不少帖子. 我在这边重新分享下自己的经验吧,将实现细节详细解释下. FlingGallery这个类摘自网上,有少许修改. 由于我需要在滑动页面时,改动title中的文字,这里采用了观察者模式,加了个OnGalleryChangeListener,有同样需求的同学可以参考下.

android屏幕监控上下左右滑动

- - CSDN博客推荐文章
简单写一下,view 或者 activity 实现 OnGestureListener 接口. 在 onFling方法中实现左右滑动:. 在 onScroll 方法中实现上下滑动:. 作者:spider_zhcl 发表于2012-6-10 12:33:34 原文链接. 阅读:12 评论:0 查看评论.

左右手的脑力较量

- 什么原因 - 果壳网 guokr.com - 果壳网
饭桌上,当你拿起筷子,左边就坐的美女发出了惊呼,“你是左利手呀. ”语气中带着点有意外和羡慕,因为“左利手更聪明”的想法早就深入人心了. 不过,这个说法有多少科学上的依据呢. 关于智商和左右利手之间的关系早在70多年以前就开始了. 1933年,巴克内尔大学的心理学家让339名新入学的男学生接受智商测试,随后,这些学生又接受了左右两只手的力量测试.

23web app实现上下左右滑动

- - CSDN博客Web前端推荐文章
转载请说明出处: http://blog.csdn.net/wowkk/article/category/1619287 (创意系列).     /*近期项目需要苹果电脑,如果您支持学生创业并愿意赞助我们一台,请联系我QQ696619,您可以提前获取16页创意文档,或者我们可以帮助开发一些小项目*/.

[原创]左右之争的根源是什么?

- Jack - 雪松
左右之争无非是自由和平等的争论,一个社会要更加尊重平等(左) 还是更加注重自由(右). 如果允许完全自由竞争,丛林法则,物竞天择,社会就会出现大的两极分化,影响人的平等;如果只追求平等,那么就必须要限制人的自由. 左派以毛泽东、社会主义为代表,更加注重平等,允许适当牺牲自由;右派以邓小平、资本主义为代表,更加注重自由,允许适当牺牲平等.

Sony PlayStation Vita 电池续航力大概 3 小时左右

- 夜の猫 - Engadget 中国版
我们已经看过不少 PS Vita 的消息,而在 TGS 2011 的会场上,也有不少 PS Vita 供我们试玩,只是它的耗电量到底多少,电池的继航力到底如何,我们很难在现场判断出来. 根据我们早前动手玩得到的讯息,PS Vita 的续航力应与第一代的 PSP 差不多,大概有 4 至 6 小时,即实际上大概是 3.5 至 5.5 小时.

50年来乔布斯一直在左右着世界经济?

- xing - cnBeta.COM
乔布斯逝世这对世界人民来说都是一个重大损失. 对于持有苹果股票的股民和苹果自己来说,损失就更加严重. 但乔布斯和世界经济的关系,你想过吗.

20岁左右年轻人找工作的技巧

- Paladin.lao - 译言-每日精品译文推荐
Today's generation of young adults are especially challenged in getting their careers off the ground as they face high unemployment rates and a bleak job market.

博海拾贝0530],别让任何人左右你的想法,你就是你自己

- 任校长 - 乐淘吧
【1】今天去超市,看到一个耍帅的猪头~当时我就喷了!!!. 【2】打死你我也不走,我是只兔子,凭嘛遛我. 【4】明星令人惊艳的过去,谁不曾美过. 【5】童鞋们,打死都不要睡下铺了. 【6】胸越小,我们的心越近~. 【7】牛B国产手机,果然不一样. 【10】世上最强的三个问题 男:我的第一个问题是,对于我第二个和第三个问题,你可不可以只用‘能’和‘不能’来回答.

Android 遥控车

- CasparZ - LinuxTOY
您确定您真的会用 Android 手机玩赛车. 16 岁的法国学生 Jonathan Rico 使用 Android 手机通过蓝牙实现了对改装玩具汽车的遥控. 操控的方式和那些标榜的智能手机游戏一样,使用重力感应,差别是这次控制的是现实世界中的遥控汽车. 收藏到 del.icio.us |.