Android優良品質のソニーのスクロールアルバム


ソニーの携帯電話はあまり売れていませんが、いくつかのものはやはりよくできています。工業デザインはもちろん、ソニーのアルバムのダブル指は任意のズーム機能も特にかっこいいです。そのデスクトップの小さい部品はアルバムを転がしてもいいと思います。Googleのオリジナルアルバムより壁の機能がよくなりました。ネットで検索しても誰かがこれを書いているのを見つけられませんでした。そこで、私の高商品を紹介します。 
まず効果図です。
 
主なジェスチャーの操作は以下の通りです。
 1.上下満速で移動し、写真をスライド/スライドさせることができます。
 2.上/下を速く読んで移動すると、スライド速度によって、上/下の複数の写真がスライドします。
 3.クリックすると、システムのライブラリに画像の展示をお願いします。
このウィジェットの主な利点は、画面内の小さな範囲で、良い画像選択/ブラウズパーツを提供しています。特に、画面切り替え時に、動画離れ感が強く、好感度がアップします。 
コード解析
この小さい部品を考え始めた時は、複数のImageViewを重ねて実現した効果だと思いました。例えば、Google原生のこの部品は複数のImageViewを重ねて形成されていますが、効果はこれに比べて遥かに遠いです。複数のImageViewを重ねていると、これほど流暢ではなく、性能的にも良くないと思います。この効果自体も規則的なので、Viewを通してより良い性能を実現できるはずです。そこでView Herarchyで分析してみると、ソニーはこれはやはりViewで実現されています。
コードは主に三つの部分から構成されています。
 •RollImageView:実際のView
 •CelCalculater:各画像の描画領域と透明度をリアルタイムで計算するために使用されます。これはこの小さい部品の核心部品です。インターフェースの定義は以下の通りです。 

/**
  * get all rects for drawing image
  * @return
  */
 public Cell[] getCells();

 /**
  *
  * @param distance the motion distance during the period from ACTION_DOWN to this moment
  * @return 0 means no roll, positive number means roll forward and negative means roll backward
  */
 public int setStatus(float distance);


 /**
  * set the dimen of view
  * @param widht
  * @param height
  */
 public void setDimen(int widht, int height);

 /**
  * set to the status for static
  */
 public void setStatic();

•ImageLoader:画像をロードするために、BitmapがRollImageViewに描画することを提供します。インターフェースの定義は以下の通りです。 

/**
  * the images shown roll forward
  */
 public void rollForward();

 /**
  * the images shown roll backward
  */
 public void rollBackward();

 /**
  * get bitmaps
  * @return
  */
 public Bitmap[] getBitmap();

 /**
  * use invalidate to invalidate the view
  * @param invalidate
  */
 public void setInvalidate(RollImageView.InvalidateView invalidate);

 /**
  * set the dimen of view
  * @param width
  * @param height
  */
 public void setDimen(int width, int height);


 /**
  * the image path to be show
  * @param paths
  */
 public void setImagePaths(List<String> paths);

 /**
  * get large bitmap while static
  */
 public void loadCurrentLargeBitmap(); 

各部分のコアコードを分析します。 
RollImageView
Viewの主な役割は、drawの各bitmapとユーザのジェスチャー操作に応答することであり、比較的簡単である。
描画部分は、ImageLoaderから得られた各BitmapをCelCalculaterから得られた描画領域及び透明度に基づいて画面に描画し、現在本コードで実現されているのは比較的簡単で、サイズの異なる画像を考慮しないで、ImageView.ScaleTypeで定義されているような、より協調的な表示方式を行う必要がある。   

@Override
 public void onDraw(Canvas canvas) {
  super.onDraw(canvas);
  Bitmap[] bitmaps = mImageLoader.getBitmap();
  Cell[] cells = mCellCalculator.getCells(); //    Image         
  canvas.translate(getWidth() / 2, 0);
  for (int i = SHOW_CNT - 1; i >= 0; i--) { //     Image    
   Bitmap bitmap = bitmaps[i];
   Cell cell = cells[i];
   if (bitmap != null && !bitmap.isRecycled()) {
    mPaint.setAlpha(cell.getAlpha());
    LOG("ondraw " + i + bitmap.getWidth() + " " + cell.getRectF() + " alpha " + cell.getAlpha());
    canvas.drawBitmap(bitmap, null, cell.getRectF(), mPaint);
   }
  }
 }

ジェスチャー部分はGesture Listenerを採用しています。主要コードは以下の通りです。 

@Override
 public boolean onTouchEvent(MotionEvent event) {
  if (event.getPointerCount() > 1) {
   return false;
  }
  mGestureDetector.onTouchEvent(event);
  switch (event.getAction()) {
   case MotionEvent.ACTION_UP: //            Fling   ,            
    if(!mIsFling){
     if(mRollResult == CellCalculator.ROLL_FORWARD){
      mImageLoader.rollForward();
     } else if (mRollResult == CellCalculator.ROLL_BACKWARD && !mScrollRollBack){
      mImageLoader.rollBackward();
     }
     LOG("OnGestureListener ACTION_UP setstatic " );
     mCellCalculator.setStatic();
     mImageLoader.loadCurrentLargeBitmap();
    }
    break;
   default:
    break;
  }
  return true;
 }

 
 //    
 @Override
 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
  mScrollDistance += distanceY;
  if(mScrollDistance > 0 && !mScrollRollBack){
   mImageLoader.rollBackward();
   mScrollRollBack = true;
  } else if(mScrollDistance < 0 && mScrollRollBack){
   mImageLoader.rollForward();
   mScrollRollBack = false;
  }
  LOG("OnGestureListener onScroll " + distanceY + " all" + mScrollDistance);
  mRollResult = mCellCalculator.setStatus(-mScrollDistance);
  invalidate();
  return true;
 }
 
 //    
 @Override
 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
  if (Math.abs(velocityY) > MIN_FLING) {
   LOG("OnGestureListener onFling " + velocityY);
   if (mExecutorService == null) {
    mExecutorService = Executors.newSingleThreadExecutor();
   }
   mIsFling = true;
   mExecutorService.submit(new FlingTask(velocityY));
  }
  return true;
 }
 
 //               Images
 private class FlingTask implements Runnable {

  float mVelocity;
  float mViewHeight;
  int mSleepTime;
  boolean mRollBackward;

  FlingTask(float velocity) {
   mRollBackward = velocity < 0 ? true : false;
   mVelocity = Math.abs(velocity / 4);
   mViewHeight = RollImageView.this.getHeight() / 2;
   mSleepTime = (int)(4000 / Math.abs(velocity) * 100); //the slower velocity of fling, the longer interval for roll
  }

  @Override
  public void run() {
   int i = 0;
   try{
    while (mVelocity > mViewHeight) {
     mCellCalculator.setStatus(mRollBackward ? -mViewHeight : mViewHeight);
     mHandler.sendEmptyMessage(MSG_INVALATE);
     //determines the count of roll. The using of mViewHeight has no strictly logical
     mVelocity -= mViewHeight;
     if (((i++) & 1) == 0) { //roll forward once for every two setStatus
      if(mRollBackward){
       mImageLoader.rollBackward();
      }else {
       mImageLoader.rollForward();
      }
     }
     Thread.sleep(mSleepTime);
    }
    mCellCalculator.setStatic();
    mImageLoader.loadCurrentLargeBitmap();
    mHandler.sendEmptyMessage(MSG_INVALATE);
   } catch(Exception e){

   } finally{

   }
  }
 }

CelCalculater分析
まず、前に移動する/後ろに移動するという概念を説明します。表示したいピクチャパスはListとして記憶され、一番前に表示されたピクチャの索引がindexであると仮定すると、現在表示されているピクチャは[index,index+3]で、前に行くとindexに1を加え、後にindexに1を減算する。
CelCalculaterの計算状況は主にユーザがジェスチャーで操作し、前または後ろに1枚の写真を移動する必要があるという意図を表している。Viewで取得できるのは、ジェスチャー移動の距離だけですので、CelCalculaterでは、移動距離を処理して移動結果を出力する必要があります。私の実現では、移動距離が写真の高さの半分を超えると、表示された画像は1ビット移動する必要があります。さもなければ、ジェスチャー操作が終了すると、Sttic状態に設定されます。主なコードは以下の通りです。 

public DefaultCellCalculator(int showCnt){
  mCnt = showCnt;
  mCells = new Cell[mCnt];
  mAlphas = new float[mCnt];
  STATIC_ALPHA = new int[mCnt];
  STATIC_ALPHA[mCnt - 1] = 0; //          0
  int alphaUnit = (255 - FIRST_ALPHA) / (mCnt - 2);
  for(int i = mCnt - 2; i >= 0; i--){ //            
   STATIC_ALPHA[i] = FIRST_ALPHA + (mCnt - 2 - i) * alphaUnit;
  }
 }

 @Override
 public Cell[] getCells() {
  return mCells;
 }
 
 //      ,distance      ,            /    
 @Override
 public int setStatus(float distance) {
  if(distance > 0){
   return calculateForward(distance);
  } else if(distance < 0){
   return calculateBackward(distance);
  } else{
   initCells();
  }
  return 0;
 }

 //  RollImageView   ,           
 @Override
 public void setDimen(int widht, int height) {
  mViewWidth = widht;
  mViewHeight = height;
  mWidhtIndent = (int)(WIDHT_INDENT * mViewWidth);
  mWidths = new int[mCnt];
  for(int i = 0; i < mCnt; i++){
   mWidths[i] = mViewWidth - i * mWidhtIndent;
  }
  //       。
  //       ,             ,             ,   mcnt-1
  mImageHeight = mViewHeight - (mCnt - 1) * HEIGHT_INDENT;
  LOG("mImageHeight " + mImageHeight);
  initCells();
 }

 
 //   ,          
 @Override
 public void setStatic() {
  initCells();
 }

 //              
 private int calculateForward(float status){
  float scale = status / mImageHeight;
  LOG("scale " + scale + " mImageHeight " + mImageHeight + " status " + status);
  for(int i = 1; i < mCnt; i++){
   mCells[i].setWidth(interpolate(scale * 3, mWidths[i], mWidths[i - 1])); // *3            ,   
   mCells[i].moveVertical(interpolate(scale * 10, 0, HEIGHT_INDENT)); //*10           ,        
   mCells[i].setAlpha((int)interpolate(scale, STATIC_ALPHA[i], STATIC_ALPHA[i - 1]));
  }
  mCells[0].moveVertical(status);
  mCells[0].setAlpha((int)interpolate(scale, 255, 0));
  if(status >= mImageHeight / 3){
   return ROLL_FORWARD;
  } else {
   return 0;
  }
 }

 //              
 private int calculateBackward(float status){
  float scale = Math.abs(status / mImageHeight);
  for(int i = 1; i < mCnt; i++){
   mCells[i].setWidth(interpolate(scale, mWidths[i - 1], mWidths[i]));
   mCells[i].moveVertical(-scale * HEIGHT_INDENT);
   mCells[i].setAlpha((int)interpolate(scale, STATIC_ALPHA[i - 1], STATIC_ALPHA[i]));
  }
  mCells[0].resetRect();
  mCells[0].setWidth(mWidths[0]);
  mCells[0].setHeight(mImageHeight);
  mCells[0].moveVertical(mImageHeight + status);
  mCells[0].setAlpha((int)interpolate(scale, 0, 255));
  if(-status >= mImageHeight / 3){
   return ROLL_BACKWARD;
  } else {
   return 0;
  }
 }

 /**
  * status without move
  */
 private void initCells(){
  int top = -HEIGHT_INDENT;
  for(int i = 0; i < mCnt; i++){
   RectF rectF = new RectF(0,0,0,0);
   rectF.top = top + (mCnt - 1 - i) * HEIGHT_INDENT;
   rectF.bottom = rectF.top + mImageHeight;
   mCells[i] = new Cell(rectF, STATIC_ALPHA[i]);
   mCells[i].setWidth(mWidths[i]);
  }
 }

 //    
 private float interpolate(float scale, float start, float end){
  if(scale > 1){
   scale = 1;
  }
  return start + scale * (end - start);
 }

ImageLoader分析
ImageLoaderは実は比較的に簡単で、主に次の2点があります。
 •ジェスチャー操作に応答して、対応する前/後ろに移動する場合のBitmap要求を処理します。
 •ジェスチャーがまだ操作されている場合は、図を読み込む必要があります。ジェスチャー操作が終わったら、大図を読み込むべきです。ゆっくり移動する場合だけは、明確な表示が必要ですが、素早く移動する場合は、図を表示すればいいので、現在のindexと前の図をロードしてください。

//    index          
 @Override
 public void loadCurrentLargeBitmap() {
  for(int i = mCurrentIndex - 1; i < mCurrentIndex + 2; i++){
   if(i >= 0 && i < mImagesCnt - 1){
    mBitmapCache.getLargeBitmap(mAllImagePaths[i]);
   }
  }
 }

 //index      
 @Override
 public void rollForward() {
  LOG("rollForward");
  mCurrentIndex++;
  if(mCurrentIndex > mImagesCnt - 1){
   mCurrentIndex = mImagesCnt - 1;
  }
  setCurrentPaths();
 }

 //index      
 @Override
 public void rollBackward() {
  LOG("rollBackward");
  mCurrentIndex--;
  if(mCurrentIndex < 0){
   mCurrentIndex = 0;
  }
  setCurrentPaths();
 }

 @Override
 public Bitmap[] getBitmap() {
  if(mCurrentPaths != null){
   LOG("getBitmap paths nut null");
   for(int i = mCurrentIndex, j = 0; j < mShowCnt; j++, i++){
    if(i >= 0 && i < mImagesCnt){
     mCurrentBitmaps[j] = mBitmapCache.getBimap(mAllImagePaths[i]);
    } else{
     mCurrentBitmaps[j] = mBitmapCache.getBimap(NO_PATH);
    }
   }
  }
  return mCurrentBitmaps;
 }

最後に、すべてのソースコード:https://github.com/willhua/RollImage
以上が本文の全部です。皆さんの勉強に役に立つように、私たちを応援してください。