가로스크롤로 그림은 넘기면서 안에 있는 내용물은 세로스크롤이 되는 갤러리입니다.
그림을 넘기는 부분까지 세로스크롤을 원한다면 Gallery를 상속받을 게 아니라 ListView를 상속받아서 사용해야합니다.
Gallery는 가로 스크롤 밖에 되지 않습니다.
동영상을 보면 이해가 가실 겁니다.
간단히 해결될 문제처럼 보이지만 그리 간단한 문제가 아닙니다.
그냥 getView에 이미지를 보여주는 게 아니라 스크롤뷰로 한번싸면 될 거 같지만, 그러면 스크롤뷰가 이벤트를 다 가져가 버려서 가로스크롤이 되지 않습니다.
dispatchTouchEvent를 쓰면 둘 다 작동은 되지만 손가락이 일직선으로 좌우로 움직이는 게 아니기 때문에 의도한대로 스크롤 되는 게 아니라
정말 손가락 움직이는대로 움직이게 됩니다. 동영상처럼 세로스크롤만 하려고 했는데 오른쪽 왼쪽에 있는 그림들이 들어갔다 나왔다 거리죠.
onTouchEvent를 상속받아서 손가락이 가로로 움직이면 갤러리에서 처리하고 세로로 움직이면 스크롤뷰에서 처리하면 될 거 같지만,
인간의 손가락은 그렇게 정확하게 움직여주지 않습니다. 가로로 움직이려고 했는데, 첫 터치는 세로로 움직일 수도 있거든요...
한가지 방법은 갤러리에서 터치이벤트를 모두 낚아채서 첫터치 이후 일정거리(예제에서는 1/15인치)를 가로로 움직이면 가로 스크롤,
세로로 움직이면 세로스크롤로 처리하는 방법이 있습니다. 주의사항은 세로스크롤로 보낼 때는 첫 이벤트인 것처럼 위장하기 위해서
마우스 이벤트를 DOWN 이벤트로 수정해줘야합니다.
# 한장씩 넘어가는 갤러리
# 어댑터안에서 메모리 관리하기
에서 중요한 소스는 설명했기 때문에 그냥 전체소스만 붙이겠습니다. 말로 하는 것보다 소스를 보시는 게 더 이해가 빠를 수 있으니까요.
OneFlingScrollGallery.java
package com.givenjazz.android;
import android.content.Context;
import android.hardware.SensorManager;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import android.widget.Gallery;
public class OneFlingScrollGallery extends Gallery {
private static final int NOTHING = 0;
private static final int HORIZONTAL = 1;
private static final int VERTICAL = 2;
private float mSensitivity;
private float mDownX;
private float mDownY;
private boolean mNeedToPosition;
private boolean mNeedToJudge;
private int mDirection;
private float mDistanceX;
private float mDeceleration;
public OneFlingScrollGallery(Context context) {
this(context, null);
}
public OneFlingScrollGallery(Context context, AttributeSet attrs) {
super(context, attrs);
float ppi = context.getResources().getDisplayMetrics().density * 160.0f;
mSensitivity = ppi/15; // 민감도 1/15인치
mDeceleration = SensorManager.GRAVITY_EARTH // g (m/s^2)
* 39.37f // inch/meter
* ppi // pixels per inch
* ViewConfiguration.getScrollFriction();
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
float toMoveDistance = getWidth() - Math.abs(mDistanceX);
float maxVelocity = (float)Math.sqrt(toMoveDistance * mDeceleration * 2);
float revisedVelocityX = 0;
if (velocityX > 0) {
revisedVelocityX = Math.min(velocityX, maxVelocity);
} else {
revisedVelocityX = Math.max(velocityX, -maxVelocity);
}
return super.onFling(e1, e2, revisedVelocityX, velocityY);
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (mNeedToPosition) {
mDistanceX = 0;
mNeedToPosition = false;
distanceX = 0;
}
mDistanceX += distanceX;
return super.onScroll(e1, e2, distanceX, distanceY);
}
@Override
public boolean onTouchEvent(MotionEvent e) {
switch (mDirection) {
case HORIZONTAL:
return super.onTouchEvent(e);
case VERTICAL:
if (mNeedToJudge == true) {
mNeedToJudge = false;
//스크롤뷰에서 처음 받을 이벤트이니 다운이벤트로 위장
e.setAction(MotionEvent.ACTION_DOWN);
}
//스크롤뷰로 이벤트 전달
getSelectedView().onTouchEvent(e);
return true;
case NOTHING:
float deltaX = Math.abs(e.getX() - mDownX);
float deltaY = Math.abs(e.getY() - mDownY);
if (deltaX > deltaY + mSensitivity)
mDirection = HORIZONTAL;
else if (deltaX + mSensitivity < deltaY)
mDirection = VERTICAL;
}
return true;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
mDirection = NOTHING;
mNeedToPosition = true;
mNeedToJudge = true;
mDownX = e.getX();
mDownY = e.getY();
return true;
}
}
ImageAdapter.java (소스의 간결함을 위해 convertView 사용은 생략)
package com.givenjazz.android;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import android.content.Context;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Gallery;
import android.widget.ImageView;
import android.widget.ScrollView;
public class ImageScrollAdapter extends BaseAdapter {
private List<Integer> mResources;
private Context mContext;
private List<WeakReference<View>> mRecycleList = new ArrayList<WeakReference<View>>();
public ImageScrollAdapter(Context c, List<Integer> resources) {
mContext = c;
mResources = resources;
}
@Override
public int getCount() {
return mResources.size();
}
@Override
public Object getItem(int position) {
return mResources.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
public void recycle() {
RecycleUtils.recursiveRecycle(mRecycleList);
}
public void recycleHalf() {
int halfSize = mRecycleList.size() / 2;
List<WeakReference<View>> recycleHalfList = mRecycleList.subList(0,
halfSize);
RecycleUtils.recursiveRecycle(recycleHalfList);
for (int i = 0; i < halfSize; i++)
mRecycleList.remove(0);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ScrollView scrollView = new ScrollView(mContext);
ImageView i = new ImageView(mContext);
try {
i.setImageResource(mResources.get(position));
} catch (OutOfMemoryError e) {
if (mRecycleList.size() <= parent.getChildCount()) {
Log.e(this + "", "size:" + mRecycleList.size());
throw e;
}
Log.w(this + "", e.toString());
recycleHalf();
System.gc();
return getView(position, scrollView, parent);
}
i.setAdjustViewBounds(true);
i.setLayoutParams(new Gallery.LayoutParams(i.getDrawable()
.getIntrinsicWidth(), i.getDrawable().getIntrinsicHeight()));
scrollView.addView(i);
mRecycleList.add(new WeakReference<View>(scrollView));
return scrollView;
}
}
라이센스는 언제나 그렇듯이 아파치2.0으로 공개합니다.
안드로이드에서 아이폰 사진첩처럼 한장씩 넘어가는 갤러리
앱 위에 보면 아이폰의 페이지 컨트롤처럼 보여지는 부분도 있는데, 그거까지 설명하면 글이 길어지니 일단 생략하겠습니다.
안드로이드 기본 뷰는 한장씩 넘어가는 게 안되서 ViewFlipper나 ViewSwitcher로 구현을 하게 되는데 손으로 드래그해서 스크롤되는 효과를 줄 수가 없게 되죠.
여기서는 Gallery뷰를 상속받아 스크롤이 미끄러질때 미끄러지는 속도를 딱 한장만 넘어갈 정도의 속도로 낮춰서 만들었습니다.
학창시절 물리시간에 배운 기억을 더듬어봐서 속도를 공식으로 표현하면 속도의 제곱은 2 * 마찰계수 * 중력가속도 * 이동거리가 됩니다.
물리에 관한 글이 아니므로 어떻게 유도했는지 자세한 설명은 생략하겠습니다.
일단 상수인 마찰계수와 중력가속도부터 구해야하는데 정보가 없으니 안드로이드 프레임워크 소스를 열어서 분석해봐야합니다.
실제로 저런식으로 구현됐는지도 확인해봐야하고요. 설마했는데 실제로 소스를 열어보니 실제로 지구의 중력가속도에 실제 픽셀을 거리로 변환해서 구현했습니다.
(이렇게 구현한 것도 사실 이해가 안가는데 토성, 목성 같은 곳의 중력가속도 정보도 있습니다;;;)
public OneFlingGallery(Context context, AttributeSet attrs) {
super(context, attrs);
float ppi = context.getResources().getDisplayMetrics().density * 160.0f;
mDeceleration = SensorManager.GRAVITY_EARTH // g (m/s^2)
* 39.37f // inch/meter
* ppi // pixels per inch
* ViewConfiguration.getScrollFriction();
}
중력가속도는 SensorManager.GRAVITY_EARTH * 39.37f * ppi
마찰계수는 ViewConfiguration.getScrollFriction() 입니다.
소스를 열어보면 마찰계수가 0.015로 되어있습니다. 저 마찰계수는 빙판과 아이스하키 공(퍽)의 마찰계수 수준입니다.
스크롤을 한번하면 멈추지 않고 쭉쭉 미끄러지는 게 이해가 가는군요.
(기껏 물리학적으로 자연스럽게 구현해서 저런식으로 부자연스럽게 낮은 마찰계수를 적용했는지는 이해가 안갑니다만)
참고로 진저나 프로요 이하에서는 마찰계수가 프레임워크에 상수로 고정되어 있으나 API 11(3.0 허니콤) 부터는 setFriction메소드로 마찰계수의 변경이 가능합니다.
그때부터는 속도가 아니라 마찰계수를 바꾸면 되니 더 자연스럽게 구현을 할 수 있을 겁니다.
이제 이동거리를 구해봅시다. 갤러리같은 어댑터뷰는 사실 스크롤이 되는 게 아니라 자식뷰들을 반대쪽으로 이동시키는 구조로 되어 있습니다.
getScrollX()로 구하면 항상 0이 나오기 때문에 onScroll을 오버로드해서 자식뷰들을 이동시킨 거리를 합해서 구해야합니다.
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
float distanceY) {
mDistanceX += distanceX;
return super.onScroll(e1, e2, distanceX, distanceY);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
mDistanceX = 0;
}
return super.onTouchEvent(event);
}
이렇게 하면 mDistanceX에 스크롤된 거리가 합산이 됩니다. onScroll을 오버로드 하지말고, onTouchEvent에서 이벤트거리만 합산해도 되지 않겠냐고 생각할지 모르지만 안됩니다. 마지막에 onScroll이벤트보다 fling이 먼저 발생하는데 그 때 1,2 픽셀정도 오차가 나게되고 한장씩 넘어가는 건 맞지만 딱 맞아떨어져서 멈추지 않기 때문에 묘하게 거슬립니다.
갤러리 크기에서 스크롤된 거리를 빼면 이동해야할 거리가 나옵니다.
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
float toMoveDistance = getWidth() - Math.abs(mDistanceX);
float maxVelocity = (float) Math.sqrt(toMoveDistance * mDeceleration
* 2);
float revisedVelocityX = 0;
if (velocityX > 0) {
revisedVelocityX = Math.min(velocityX, maxVelocity);
} else {
revisedVelocityX = Math.max(velocityX, -maxVelocity);
}
return super.onFling(e1, e2, revisedVelocityX, velocityY);
}이렇게 구한 이동해야할 거리로 속도를 구합니다. 속도를 자연스럽게 구현할 때 속도를 낮추는 건 괜찮지만 빠르게 하는 건 이상하겠죠? 자연스럽게 하기위해 속도는 원래 속도와 이동해야할 속도 중 낮은 속도로 수정해줍니다. 이렇게 해서 가다가 멈춰도 알아서 적당한 위치로 붙기 때문에 속도를 낮춰도 괜찮습니다.
그리고 그렇게 구한 속도를 fling에 넣어주면 됩니다.
아래의 전체소스이용해서 갤러리를 만드시면 되고 사용법은 기존의 Gallery 뷰와 동일합니다.
|
첨부된 파일은 동영상처럼 아이폰의 페이지 컨트롤도 포함시켰고, 바로 전에 썼던 메모리관리 기법도 적용한 예제소스입니다.
(이 부분도 설명할 부분이 있는데, 너무 방대해져서 그냥 소스만 첨부합니다. 나중에 기회되면 어댑터 내에서 메모리 관리하는 것도 설명할게요)
화면에 꽉차는 이미지 등장할 때부터 부터는 이미지가 좀 늦게 등장하는 현상이 있는데 그림파일 I/O 때문에 발생합니다.
다음에 I/O시간 때문에 UI가 멈추는 시간을 제거하는 기법을 설명하거나 갤러리 안에서 자연스럽게 세로스크롤 되는 기법을 설명하겠습니다.
(첨부된 소스 안에 oneFlingScrollGallery는 세로스크롤 하는 갤러리 소스입니다)
소스가 첨부된 GitHub 링크
어댑터 안에서 메모리 관리하기.
얼마 전에 내 블로그에 안드로이드 메모리 누수 줄이기 포스팅을 했는데, 어댑터 뷰에서 관리하는 법을 예제와 곁들어 추가로 설명할까 합니다.
어댑터뷰 안에 데이터는 실질적으로 어댑터가 관리를 하기 되기에 메모리 관리하는 부분도 어댑터 안에서 관리하는 게 좀 더 수월합니다.
가비지 콜렉터가 어느정도 알아서 관리를 해주나, 이미지처럼 메모리를 많이 사용할 경우 빠르게 해제하기 위해 직접관리를 해야하는 경우도 생깁니다.
다음 동영상은 저번에 올렸던 한 장씩 넘어가는 갤러리뷰에다가 고용량의 이미지 파일을 넣어서 메모리 관리가 되고 있는 모습입니다.
(예를 들기 위해 오버했습니다. 실제로는 당연히 모바일 환경에 맞게 이미지를 줄여서 써야겠죠. 이미지 읽어들이는 시간도 꽤 길어서 멈추는 느낌이 드는데
이거 해결하는 기법은 포스팅하겠습니다) 바탕화면급 고용량 이미지라 빠르게 3~4장 정도만 이미지를 읽어도
java.lang.OutOfMemoryError: bitmap size exceeds VM budget이 발생합니다.
동영상보면 빨간색과 주황색 로그가 올라가는데 그게 Error가 발생한 부분이고 동영상은 메모리를 해제해주고 계속 작동 시키는 모습을 볼 수 있습니다.
어댑터에서 구현한 부분은 다음 부분입니다.
// 액티비티에서 어댑터를 메모리에서 해제하기 위해쓰는 메소드.
// 어댑터를 사용하는 액티비티의 onDestroy에 넣어주면 된다.
public void recycle() {
RecycleUtils.recursiveRecycle(mRecycleList);
}
// 만들었던 뷰 목록 중 반을 지우는 메소드
public void recycleHalf() {
int halfSize = mRecycleList.size() / 2;
List<WeakReference<View>> recycleHalfList = mRecycleList.subList(0,
halfSize);
RecycleUtils.recursiveRecycle(recycleHalfList);
for (int i = 0; i < halfSize; i++)
mRecycleList.remove(0);
}
여기 RecycleUtils.recursiveRecycle은 전에 올렸던 메모리 누수 관리하기를 참고해주세요.
/* * Copyright (C) 2010 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. * * author by givenjazz */ package com.givenjazz.android; import java.lang.ref.WeakReference; import java.util.List; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ImageView; /** * @author givenjazz * */ public class RecycleUtils { private RecycleUtils() { }; public static void recursiveRecycle(View root) { if (root == null) return; root.setBackgroundDrawable(null); if (root instanceof ViewGroup) { ViewGroup group = (ViewGroup) root; int count = group.getChildCount(); for (int i = 0; i < count; i++) { recursiveRecycle(group.getChildAt(i)); } if (!(root instanceof AdapterView)) { group.removeAllViews(); } } if (root instanceof ImageView) { ((ImageView) root).setImageDrawable(null); } root = null; return; } public static void recursiveRecycle(List<WeakReference<View>> |
WeakReference로는 레퍼런스를 해도 가비지 콜렉터할 때 신경을 안쓰기 때문에 가비지콜렉팅 대상에 포함됩니다.
예를 들어 Reference가 아니라 그냥 List에 View를 포함했다면 어댑터에서 자동으로 메모리 해제를 안하기 때문에 쉽게 메모리 오류가 발생할 겁니다.
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ImageView i = new ImageView(mContext);
try {
i.setImageResource(mResources.get(position));
} catch (OutOfMemoryError e) {
if (mRecycleList.size() <= parent.getChildCount()) {
Log.e(this + "", "size:" + mRecycleList.size());
throw e;
}
Log.w(this + "", e.toString());
recycleHalf();
System.gc();
return getView(position, convertView, parent);
}
i.setAdjustViewBounds(true);
i.setLayoutParams(new Gallery.LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT));
mRecycleList.add(new WeakReference<View>(i));
return i;
이미지를 읽다가 OutOfMemoryError 예외가 발생하면 전에 어댑터에서 사용했던 뷰의 반을 메모리에서 해제하고, 재귀로 getView를 호출합니다.
오류가 안 날때까지 반씩 줄이다가 어댑터뷰의 크기만큼 줄였는데도 메모리오류가 나면 그냥 오류던지고 종료시키도록 구현했는데,
이 부분은 상황에 맞게 처리해주시면 되겠죠. 이건 그냥 메모리를 관리하는 하나의 예제일 뿐입니다.
OutOfMemoryError가 자주 난다면 try/catch로 잡는 것보다 mRecycleList의 크기를 비교해서 미리 해제해주는 등의 더 섬세하게 관리를 해주셔야 될겁니다.
갤러리는 가로스크롤 되고 안에 있는 내용이 세로스크롤 되는 뷰는 다음에 설명하도록 하겠습니다.
완전히 세로로 스크롤 되는 건 원하시면 그냥 리스트뷰 쓰시면 되겠습니다.
|
소스는 이미지 리소스만 다르고 전에 올렸던 안드로이드 아이폰처럼 한장씩 넘기를 갤러리와 동일해서 생략하겠습니다.
'IT_Programming > Android_Java' 카테고리의 다른 글
[android] 애니메이션 - 비밀번호창 좌우로 7번 흔들기 (0) | 2011.08.01 |
---|---|
[펌] ViewFlipper와 ScrollView, ListView 를 함께 사용할때 생기는 문제 해결책 (0) | 2011.07.22 |
FaceBook API 연동 (0) | 2011.07.04 |
[펌] Android Thread 구현 (0) | 2011.07.01 |
[펌] 안드로이드 App과 Twitter API 연동하기 (0) | 2011.07.01 |