Android学习之优化美女图片浏览器

接上一篇文章:Android学习之打造美女图片浏览器

上一篇博文其实我给大家留了好多坑,那么这篇文章我就一一把坑都埋了,好吧:)有一种自己挖坑自己埋的感觉,不过埋的过程我们也是可以学到不少的;而且我比较喜欢诱导大家来完成,发现问题解决问题本来就是编程中每天遇到的;

 

正所谓一个萝卜一个坑,填坑还得踏踏实实的来;

坑一:图片错位;坑二popwindow弹出位置;

当然这是上一篇文章留的坑,接下来在完成的时候我们可能还会遇到坑三坑四,不过这篇文章都会解决掉;

 

坑一:图片错位

首先我们要分析到底是image-loader的问题还是GridView的问题;其实不难发现,MyGridViewAdapterview有缓存机制并且我们也用了;由于getView在滑动的时候会将消失的converView复用到了新的位置,又由于image-loader采用异步加载的方式,所以先显示原来位置的图片,等加载完成在显示新的图片;查看详细请移至图片错位解决办法

解决:我们采用image-loaderloadImage方法,在加载之前先显示一张占位图,待加载完成再显示并且加入渐入动画,这样体验就更好了;

MyGridViewAdapter.java

/**
	 * 自己去加载就得处理图片错位了
	 * 
	 * @param imageView
	 * @param position
	 */
	private void loadImage(final ImageView imageView, final int position) {
		Animation animation = AnimationUtils.loadAnimation(mContext,
				R.anim.anim_alpha);
		imageView.startAnimation(animation);
		Logs.i("width:" + imageView.getWidth() + ";height:"
				+ imageView.getHeight());
		ImageLoaderUtil.loadImage(getItem(position).getUrl(), new ImageSize(
				imageView.getWidth(), imageView.getHeight()), ImageLoaderUtil
				.getDefaultOptions(), new SimpleImageLoadingListener() {
			@Override
			public void onLoadingComplete(String imageUri, View view,
					Bitmap loadedImage) {
				/**
				 * 如下判断就是处理图片在快速滑动异步线程不断执行setImageBitmap
				 * 通过比较当前的url是否过期,来给imageView设置一次也就是最新的Bitmap
				 * 这样就避免了多次重复的setImageBitmap,而且加了渐变动画体验就会更好了
				 */
				if (imageUri.equals(imageView.getTag(R.id.imageView))) {
					imageView.setImageBitmap(loadedImage);
				}
			}

			@Override
			public void onLoadingStarted(String imageUri, View view) {
				super.onLoadingStarted(imageUri, view);
				imageView.setImageResource(R.drawable.empty_photo);
			}
		});
	}

代码中有详细注释,这里就不多介绍了;

运行测试:


上图不知道大家发现一个问题没有,当我们点击加载更多的时候全屏会闪动一下;可能有人已经知道了,我们点击了加载更多调用了Adapter.notifyDataSetChanged方法,当前屏幕每一个item都会再一次调用getView,也就是又重新设置了一遍Bitmap,并且做了一次渐入的动画,所以说这个坑还是比较好理解的;(ps:豆瓣app中查看Top250列表的时候点击加载更多也会一闪,原因就在于此,本想这个坑可解可不解,但是为了体验为了承诺拼了;csdn要是能发表情就好了,虽然是技术博客,但是也要任性也要卖萌啊;)

 

坑三:点击加载更多GridView刷新问题

解决:加入如下代码

MyGridViewAdapter.java

/**
	 * 给convertView设置数据
	 * 
	 * @param position
	 * @param convertView
	 */
	private void setDataToConvertView(int position, View convertView) {
		if (getItemViewType(position) == VIEW_TYPE_ITEM) {
			ViewHolder holder = (ViewHolder) convertView.getTag();
			/**
			 * 此处的判断是为了避免调用notifyDataSetChanged所有的view都要重新setImageBitmap
			 * 导致闪动也就是做了我们的渐入动画
			 */
			if (!getItem(position).getUrl().equals(
					holder.mImageView.getTag(R.id.imageView))) {
				Log.d("MyGrid", "position:" + position);
				holder.mImageView.setTag(R.id.imageView, getItem(position)
						.getUrl());
				holder.mImageView.setTag(R.id.gridView, position);

				// 1displayImage 两种方式的比较
				// displayImage(holder.mImageView, position);
				// 2loadImage
				loadImage(holder.mImageView, position);
			}
		} else if (getItemViewType(position) == VIEW_TYPE_FOOT && position != 0) {
			setFooterViewStatus(FooterView.MORE);
		}
	}

上述主要就是为了避免返回的缓存视图和当前要设置的视图如果是同样的我们还要再一次loadImage,这样就避免了闪动;

再此运行看效果,恩不错


 

隐藏坑四:本以为图片加载已万事大吉的时候,一个非常隐藏的bug出现了,看截图;


这个bug的复现过程是这样的,当我滑动到底部然后再回到顶部,然后更改美女图类型的时候就会出现第一张图片加载失败的过程;并且该图还没被放到内存中的时候会出现;

 

这个bug我真的是找了好久,最后问题锁定在了当position=0的时候调用多次,查了好多资料说是防止他加载多次还是无用;就算position=0调用多次可是设置最后一次总是正确的,打印log可以得出这个结论;

 

最后终于发现原来是loadImage的回调方法onLoadingComplete没有执行;有的时候position=0执行多次,前一次的ImageView对象和正确的ImageView对象不是同一个,但是他们会加载相同的url,这样导致首先调用的回调onLoadingComplete而后调用的不会回调;

原来这是image-loader的一个机制,这个到不算bug;他的作者在听了读者的反馈说希望对重复url过滤掉,结果就导致了我们现在的这个隐藏bug;

解决:

1. 我们可以用displayImage这个方法,因为他已经解决了这个问题(ps:不过得把diaplayoption加上)

2. 我们还是想用loadImage去加载,要么改变position=0多次调用,不好意思这个我还真没法改,不管是设置布局为match_parent还是什么,可能由于我的底部布局为wrap_content吧(ps:改了貌似也没用);还有我觉得我也是贱非要用loadImage方法,没办法人至贱则无敌吗!!!

修改源码让他回调onLoadingComplete;

好吧接下来我们就看看image-loader的源码了,其实追踪很简单只要跟着方法不断点进去就好;

首先我们用到了loadImage这个方法找到源码如下

ImageLoader.java

public void loadImage(String uri, ImageSize targetImageSize, DisplayImageOptions options,
			ImageLoadingListener listener) {
		loadImage(uri, targetImageSize, options, listener, null);
	}
点入他真正调用的
public void loadImage(String uri, ImageSize targetImageSize, DisplayImageOptions options,
			ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
		checkConfiguration();
		if (targetImageSize == null) {
			targetImageSize = configuration.getMaxImageSize();
		}
		if (options == null) {
			options = configuration.defaultDisplayImageOptions;
		}

		// 创建携带要显示图片信息的ImageAware
		NonViewAware imageAware = new NonViewAware(uri, targetImageSize, ViewScaleType.CROP);
		// 真正要显示图片的方法
		displayImage(uri, imageAware, options, listener, progressListener);
	}
要记住这里他创建了NonViewAware对象,继续查找

public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
			ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
		checkConfiguration();
		if (imageAware == null) {
			throw new IllegalArgumentException(ERROR_WRONG_ARGUMENTS);
		}
		if (listener == null) {
			listener = defaultListener;
		}
		if (options == null) {
			options = configuration.defaultDisplayImageOptions;
		}

		// 判断uri是否为空
		if (TextUtils.isEmpty(uri)) {
			engine.cancelDisplayTaskFor(imageAware);
			listener.onLoadingStarted(uri, imageAware.getWrappedView());
			if (options.shouldShowImageForEmptyUri()) {
				imageAware.setImageDrawable(options.getImageForEmptyUri(configuration.resources));
			} else {
				imageAware.setImageDrawable(null);
			}
			listener.onLoadingComplete(uri, imageAware.getWrappedView(), null);
			return;
		}

		ImageSize targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize());
		String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
		// 将imageAware和memoryCacheKey(也就是uri)存入ImageLoaderEngine(注:此处为重要步骤)
		engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);

		listener.onLoadingStarted(uri, imageAware.getWrappedView());

		Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
		if (bmp != null && !bmp.isRecycled()) {// 判断是否在缓存中
			L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey);

			if (options.shouldPostProcess()) {
				ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
						options, listener, progressListener, engine.getLockForUri(uri));
				ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, imageLoadingInfo,
						defineHandler(options));
				if (options.isSyncLoading()) {
					displayTask.run();
				} else {
					engine.submit(displayTask);
				}
			} else {
				options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
				listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
			}
		} else {// bitmap不在缓存中
			if (options.shouldShowImageOnLoading()) {
				imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources));
			} else if (options.isResetViewBeforeLoading()) {
				imageAware.setImageDrawable(null);
			}

			ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
					options, listener, progressListener, engine.getLockForUri(uri));
			LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,
					defineHandler(options));
			if (options.isSyncLoading()) {// 同步加载
				displayTask.run();
			} else {// 异步加载(真正要加载的任务)
				engine.submit(displayTask);
			}
		}
	}
上面的注释有两点需要注意 engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);我们查看看看他到底是如何保存的
ImageLoaderEngine.java

private final Map<Integer, String> cacheKeysForImageAwares = Collections
			.synchronizedMap(new HashMap<Integer, String>());
...
void prepareDisplayTaskFor(ImageAware imageAware, String memoryCacheKey) {
		cacheKeysForImageAwares.put(imageAware.getId(), memoryCacheKey);
	}
NonViewAware.java

@Override
	public int getId() {
		return TextUtils.isEmpty(imageUri) ? super.hashCode() : imageUri.hashCode();
	}
其实看到这里是不是有一种豁然开朗的感觉啊,保存了半天就是把imageUri的hashCode作为 key,imageUri本身作为 value保存到了HashMap中;我们知道Map不能有重复的key,而相同的url的hashCode又是一样的,所以相同的url只能在Map中存在一份;其实这里还没用到,我们姑且先记住;

接下来我们再回到displayImage方法,找到真正要调用的任务其实也就是LoadAndDisplayImageTask.run方法了

@Override
	public void run() {
		if (waitIfPaused()) return;
		if (delayIfNeed()) return;

		...
		// 显示图片任务
		DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
		runTask(displayBitmapTask, syncLoading, handler, engine);
	}
我省略了一些代码都是无关的,因为我们图片要从磁盘中加载,那么继续看DisplayBitmapTask.run

@Override
	public void run() {
		if (imageAware.isCollected()) {// 此处用到的是NonImageAware,isCollected返回false
			L.d(LOG_TASK_CANCELLED_IMAGEAWARE_COLLECTED, memoryCacheKey);
			listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
		} else if (isViewWasReused()) {
			L.d(LOG_TASK_CANCELLED_IMAGEAWARE_REUSED, memoryCacheKey);
			listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
			// 加入回调,即使被复用也回调该方法
			listener.onLoadingComplete(imageUri, imageAware.getWrappedView(), bitmap);
		} else {
			L.d(LOG_DISPLAY_IMAGE_IN_IMAGEAWARE, loadedFrom, memoryCacheKey);
			displayer.display(bitmap, imageAware, loadedFrom);
			// 第一次加载完的uri,engine会从Map去移除
			engine.cancelDisplayTaskFor(imageAware);
			listener.onLoadingComplete(imageUri, imageAware.getWrappedView(), bitmap);
		}
	}

	/** Checks whether memory cache key (image URI) for current ImageAware is actual */
	private boolean isViewWasReused() {
		// 由于相同uri已被移除,所以这里返回为null
		String currentCacheKey = engine.getLoadingUriForView(imageAware);
		return !memoryCacheKey.equals(currentCacheKey);
	}
诶,累死了,总算是让问题得到了进一步缓解,重新编译image-loader

运行效果如下:

其实我的这个办法不算是最好的,还是强烈建议直接用displayImage这个便利方法,我的做法只是希望大家在用开源框架的时候要做到基本的了解才能用好;

显示图片的坑算是基本上解决了,可是有没有新坑呢?这个我还真不好说,程序就是这样只有不断优化才能趋于完美,但是不会存在0bug;升级的空间当然还是有的,不过需要大家去发掘了;

 

坑二:

回头我们该处理坑二了,主要就是popwindow下滑动画从Toolbar上方就显示了,其实这个很容易理解popwindow本来就是显示在activity的上方的;

解决:

换个思路你就明了了,我们不给popwindow做动画而是去让他内部的视图去做动画似乎这个事情就明了了;好吧分分钟实现之:

 MorePopWindow.java

	/**
	 * 重写该方法,让内部视图在显示的时候加入出场动画
	 */
	@Override
	public void showAsDropDown(View anchor) {
		super.showAsDropDown(anchor);
		Animation animation = AnimationUtils.loadAnimation(mContext,
				R.anim.slide_top_in);
		mMoreView.startAnimation(animation);
	}

	/**
	 * 重写该方法,当内部视图动画结束的时候再调用父类的dismiss方法
	 */
	@Override
	public void dismiss() {
		Animation animation = AnimationUtils.loadAnimation(mContext,
				R.anim.slide_top_out);
		mMoreView.startAnimation(animation);
		animation.setAnimationListener(new AnimationListener() {

			@Override
			public void onAnimationStart(Animation animation) {
			}

			@Override
			public void onAnimationRepeat(Animation animation) {
			}

			@Override
			public void onAnimationEnd(Animation animation) {
				new Handler().post(new Runnable() {

					@Override
					public void run() {
						cancel();
					}
				});
			}
		});
	}

	private void cancel() {
		super.dismiss();
	}

运行测试:


哈哈是不是很perfect呢!



讲到这里我也累了,其实该说的也都说了,但是有好多细节我真的没来的及说,整个demo算是到此结束了;当然这个demo或是app还有有很多的不足之处的,例如:

  • 没有加入收藏,保存到本地及分享的功能;
  • 查看大图没有多点触控放大缩小(ps:有开源框架photoView可以借鉴之,当然你也可以自己实现)
  • 等等等...

 

最后

最后呢总该聊一聊,这是必然的哈哈;经过我们埋坑的过程相信你也学到了不少;我极力强调一点:傻瓜都会编程,可是让人看了舒服的代码确实一件难事;我是见过一个类有一万多行,也见过一个方法嵌套了n多内部类;当见过这样的代码后你就会产生一种冲动...

每个人编程的出发点是不同的,不管哪种都希望大家bug没有,代码清晰哈哈...也是希望有志于软件行业的小伙伴们多看看代码整洁之道和重构这些书籍;


最后的最后附上源码地址:

https://github.com/xiaozhi003/BeautyGallery

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值