DIYフォトウィジェット





こんにちは、親愛なるhabrasociety。 カスタムロック解除画面に関する私の以前の記事は、コメントの形でレビューをほとんど受けませんでしたが、それでも、100人の人々がそれをお気に入りに保管しました。



Xperiaスマートフォンの多くのユーザーは、美しい3次元の標準写真ウィジェットを好みます。 Androidの用語では、これはAppWidgetではなく、ウィジェットに非常によく似たシンプルなビューです。 標準のXperia Homeランチャーの「プラグイン」を一気に呼び出すことができるため、他のランチャーのウィジェットのリストには含まれていません。 この投稿では、同様のウィジェットを作成する方法を説明します。



はじめに



ウィジェットをサポートする独自のAndroidランチャーを作成した場合の状況を想像してください(おそらく次の記事でこれを行う方法を簡単に説明します)。 3次元の写真ウィジェットは、他の背景に対してランチャーを強調表示します。 標準のXperia写真ウィジェットと同様に、ウィジェットはランチャーのプロセスで起動されるカスタムビューです。



ウィジェットベース



最初のステップは、使用可能なウィジェットのリストにウィジェットが表示されるようにすることです(ウィジェットではなく、思い出します)。 Sony Ericssonはまさにそれを行いました。インストールされたパッケージのリストから標準のランチャーが「com.sonyericsson.advancedwidget」で始まるパッケージを選択し、ウィジェットリストに名前とアイコンを追加します。 この方法は簡単ですが、欠点が1つあります。1つのAPK-1つのウィジェットです。 より賢くします(願っています)-将来のウィジェットのマニフェストで次のように書きます:



<receiver android:name=".PhotoWidgetReceiver" > <intent-filter> <action android:name="by.arriva.ADVANCED_WIDGET" /> </intent-filter> </receiver>
      
      





PhotoWidgetReceiverクラスは空のままになります。



 public class PhotoWidgetReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { } }
      
      





このトリックのおかげで、ランチャーは「by.arriva.ADVANCED_WIDGET」に応答するすべてのBroadcastRecieverを見つけます。 以下は、ランチャーからのコードのリストです。



 final PackageManager manager = getPackageManager(); Intent i = new Intent("by.arriva.ADVANCED_WIDGET"); List<ResolveInfo> lri = manager.queryBroadcastReceivers(i, 0); for(ResolveInfo ri : lri){ String packageName = ri.activityInfo.packageName; String className = ri.activityInfo.name; className = className.replace("Receiver", ""); AdvancedWidgetInfo awi = new AdvancedWidgetInfo(this, packageName, className); if(!awi.isValid) continue; try { WidgetInfo wi = new WidgetInfo(true, awi.label, awi.width+" x "+awi.height, manager.getResourcesForApplication(packageName).getDrawable(awi.preview), new ComponentName(packageName, className)); widgetsSorted.add(wi); } catch (Exception e) {} awi = null; } lri.clear();
      
      





文字列className-BroadcastRecieverから継承したクラスの名前。この場合はPhotoWidgetReceiverです。 ランチャーは、ウィジェットの説明を含むPhotoWidgetクラスのウィジェットを検索します。 その内容は次のとおりです。



 public class PhotoWidget { public static View getView(Context context){ WidgetView wv = new WidgetView(context); return wv; } public static int getCellWidth(Context context){ return 2; } public static int getCellHeight(Context context){ return 2; } public static String getLabel(Context context){ return context.getString(R.string.app_name); } public static int getIconId(Context context){ return R.drawable.icon; } public static int getPreviewId(Context context){ return R.drawable.preview; } }
      
      





このクラスの構造は、すべての非標準(高度)ウィジェットで厳密に一定でなければなりません。 メソッドの同じ構造は、ランチャーのAdvancedWidgetInfoクラスにあります。



直接表示



ウィジェットのメインクラス
 public class WidgetView extends View { ContentObserver observer; GestureDetector gd; Scroller scroller; NinePatchDrawable frame; Bitmap loading; Bitmap broken; Item[] items = null; float touchLastY = 0; float touchDownY = 0; boolean touchTap = false; boolean touchScroll = false; long loaderId = -1; int getScrollY = 0; int width = 160; int height = 200; public WidgetView(Context context) { super(context); gd = new GestureDetector(context, new GestureListener()); scroller = new Scroller(context); observer = new ContentObserver(new Handler()){ @Override public void onChange(boolean selfChange){ super.onChange(selfChange); loadThumbnails(); } }; frame = (NinePatchDrawable)getResources().getDrawable(R.drawable.frame); loading = getBmp(R.drawable.loading); broken = getBmp(R.drawable.broken); } @Override public void onAttachedToWindow(){ super.onAttachedToWindow(); loadThumbnails(); getContext().getContentResolver().registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true, observer); getContext().getContentResolver().registerContentObserver(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true, observer); } @Override public void onDetachedFromWindow(){ super.onDetachedFromWindow(); getContext().getContentResolver().unregisterContentObserver(observer); } @Override protected void onDraw(Canvas canvas){ super.onDraw(canvas); Paint p = new Paint(); p.setDither(true); p.setFilterBitmap(true); int pos = getSelected(); float percent = getSelectedPercent(); if(items != null){ if(items.length == 0){ Bitmap camera = getBmp(R.drawable.camera); Transformation transform = new Transformation(0, percent, 160, 140); p.setAlpha(transform.alpha); canvas.drawBitmap(camera, transform.matrix3d, p); return; } if(pos >= 1){ if(items[pos-1] != null){ if(items[pos-1].thumbnail != null){ Bitmap thumbnail = items[pos-1].thumbnail; Transformation transform = new Transformation(-1, percent, thumbnail.getWidth(), thumbnail.getHeight()); p.setAlpha(transform.alpha); canvas.drawBitmap(thumbnail, transform.matrix3d, p); } else { Transformation transform = new Transformation(-1, percent, broken.getWidth(), broken.getHeight()); p.setAlpha(transform.alpha); canvas.drawBitmap(broken, transform.matrix3d, p); } } else { Transformation transform = new Transformation(-1, percent, loading.getWidth(), loading.getHeight()); p.setAlpha(transform.alpha); canvas.drawBitmap(loading, transform.matrix3d, p); } } if(items.length-1 >= pos+1){ if(items[pos+1] != null){ if(items[pos+1].thumbnail != null){ Bitmap thumbnail = items[pos+1].thumbnail; Transformation transform = new Transformation(1, percent, thumbnail.getWidth(), thumbnail.getHeight()); p.setAlpha(transform.alpha); canvas.drawBitmap(thumbnail, transform.matrix3d, p); } else { Transformation transform = new Transformation(1, percent, broken.getWidth(), broken.getHeight()); p.setAlpha(transform.alpha); canvas.drawBitmap(broken, transform.matrix3d, p); } } else { Transformation transform = new Transformation(1, percent, loading.getWidth(), loading.getHeight()); p.setAlpha(transform.alpha); canvas.drawBitmap(loading, transform.matrix3d, p); } } if(items.length-1 >= pos){ if(items[pos] != null){ if(items[pos].thumbnail != null){ Bitmap thumbnail = items[pos].thumbnail; Transformation transform = new Transformation(0, percent, thumbnail.getWidth(), thumbnail.getHeight()); p.setAlpha(transform.alpha); canvas.drawBitmap(thumbnail, transform.matrix3d, p); } else { Transformation transform = new Transformation(0, percent, broken.getWidth(), broken.getHeight()); p.setAlpha(transform.alpha); canvas.drawBitmap(broken, transform.matrix3d, p); } } else { Transformation transform = new Transformation(0, percent, loading.getWidth(), loading.getHeight()); p.setAlpha(transform.alpha); canvas.drawBitmap(loading, transform.matrix3d, p); } } } else { Transformation transform = new Transformation(0, 0, loading.getWidth(), loading.getHeight()); canvas.drawBitmap(loading, transform.matrix3d, null); } } @Override public boolean onTouchEvent(MotionEvent me) { if(items == null) return true; gd.onTouchEvent(me); float touchY = me.getY(); if(me.getAction() == MotionEvent.ACTION_DOWN){ if(!scroller.isFinished()){ scroller.abortAnimation(); } else { touchTap = true; touchScroll = false; } touchDownY = touchLastY = touchY; } else if(me.getAction() == MotionEvent.ACTION_MOVE){ if(Math.abs(touchY - touchDownY) > ViewConfiguration.getTouchSlop() || touchScroll){ getParent().requestDisallowInterceptTouchEvent(true); touchTap = false; touchScroll = true; getScrollY += -(int)(touchY - touchLastY); computeOverscroll(); } touchLastY = touchY; } else if((me.getAction() == MotionEvent.ACTION_UP || me.getAction() == MotionEvent.ACTION_CANCEL) && scroller.isFinished()){ if(touchTap && me.getAction() == MotionEvent.ACTION_UP){ tap(); } setSelected(getSelected(), true); getParent().requestDisallowInterceptTouchEvent(false); } return true; } @Override public void computeScroll(){ if(scroller.computeScrollOffset()){ int oldY = getScrollY; getScrollY = scroller.getCurrY(); if(oldY != getScrollY){ onScrollChanged(0, getScrollY, 0, oldY); } if(scroller.getFinalY() == getScrollY){ scroller.abortAnimation(); setSelected(getSelected(), true); } postInvalidate(); } } @Override protected void onMeasure(int wms, int hms){ width = MeasureSpec.getSize(wms); height = MeasureSpec.getSize(hms); super.onMeasure(wms, hms); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh){ width = w; height = h; super.onSizeChanged(w, h, oldw, oldh); } void loadThumbnails(){ items = null; getScrollY = 0; invalidate(); Thread loader = new Thread(new Runnable() { public void run() { long currentId = Thread.currentThread().getId(); try{ final ArrayList<MediaItem> list = mediaList(); if(list == null || loaderId != currentId) return; items = new Item[list.size()]; postInvalidate(); for(int i=0; i<list.size(); i++){ if(items == null || loaderId != currentId) return; items[i] = new Item(list.get(i).path, getBmp(list.get(i)), list.get(i).type); postInvalidate(); } } catch(Exception e){} } }); loaderId = loader.getId(); loader.start(); } int getSelected(){ return Math.round(getScrollY/140f); } float getSelectedPercent(){ float f = getScrollY/140f; return f-getSelected(); } void setSelected(int pos, boolean anim){ if(anim && getScrollY!=pos*140){ scroller.startScroll(0, getScrollY, 0, pos*140-getScrollY, 250); } else { getScrollY = pos*140; } invalidate(); } void computeOverscroll(){ if(getScrollY < -50){ getScrollY = -50; } else if(getScrollY > Math.max(items.length-1, 0)*140+50){ getScrollY = Math.max(items.length-1, 0)*140+50; } invalidate(); } void tap(){ try{ if(items != null){ if(items.length == 0){ Intent i = new Intent("android.media.action.IMAGE_CAPTURE"); i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); getContext().startActivity(i); return; } if(items[getSelected()] == null) return; int type = items[getSelected()].type; String path = items[getSelected()].path; if(type == 1){ Intent view = new Intent(android.content.Intent.ACTION_VIEW); File imageFile = new File(path); view.setDataAndType(Uri.fromFile(imageFile), "image/*"); view.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); getContext().startActivity(view); } else if(type == 2){ Intent view = new Intent(android.content.Intent.ACTION_VIEW); File imageFile = new File(path); view.setDataAndType(Uri.fromFile(imageFile), "video/*"); view.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); getContext().startActivity(view); } } } catch(Exception e){} } ArrayList<MediaItem> mediaList(){ try{ ArrayList<MediaItem> list = new ArrayList<MediaItem>(); Cursor c = getContext().getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new String[]{MediaStore.Images.Media.DATA, MediaStore.Images.Media.DATE_ADDED}, null, null, null); if(c.moveToFirst()){ do{ list.add(new MediaItem(c.getString(c.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)), c.getString(c.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_ADDED)), 1)); } while(c.moveToNext()); } c.close(); c = getContext().getContentResolver().query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, new String[]{MediaStore.Video.Media.DATA, MediaStore.Video.Media.DATE_ADDED}, null, null, null); if(c.moveToFirst()){ do{ list.add(new MediaItem(c.getString(c.getColumnIndexOrThrow(MediaStore.Video.Media.DATA)), c.getString(c.getColumnIndexOrThrow(MediaStore.Video.Media.DATE_ADDED)), 2)); } while(c.moveToNext()); } c.close(); Collections.sort(list, new Comparator<MediaItem>(){ @Override public int compare(MediaItem mi1, MediaItem mi2) { return mi1.date.compareToIgnoreCase(mi2.date); } }); Collections.reverse(list); return list; } catch(Exception e){ return new ArrayList<MediaItem>(); } } Bitmap getBmp(MediaItem mi){ try{ Paint p = new Paint(); p.setDither(true); p.setFilterBitmap(true); if(mi.type == 1){ int rot = 0; try{ ExifInterface rote = new ExifInterface(mi.path); int r = rote.getAttributeInt(ExifInterface.TAG_ORIENTATION, 1); if(r == 3){ rot = 180; } else if(r == 8){ rot = 270; } else if(r == 6){ rot = 90; } else { rot = 0; } } catch(Exception e){} BitmapFactory.Options bfo = new BitmapFactory.Options(); bfo.inJustDecodeBounds = true; BitmapFactory.decodeFile(mi.path, bfo); if(rot == 90 || rot == 270){ bfo.inSampleSize = Math.min(bfo.outWidth/140, bfo.outHeight/120); } else { bfo.inSampleSize = Math.max(bfo.outWidth/140, bfo.outHeight/120); } bfo.inJustDecodeBounds = false; Bitmap src = BitmapFactory.decodeFile(mi.path, bfo); if(rot != 0){ Matrix m = new Matrix(); m.postRotate(rot); src = Bitmap.createBitmap(src, 0, 0, src.getWidth(), src.getHeight(), m, true); } float aspect = (float)src.getWidth()/src.getHeight(); int image_w = 160; int image_h = 140; if(aspect > 1){ image_h = (int)((image_w-20)/aspect+20); } else { image_w = (int)((image_h-20)*aspect+20); } Bitmap result = Bitmap.createBitmap(image_w, image_h, Config.ARGB_8888); Canvas c = new Canvas(result); frame.setBounds(0, 0, image_w, image_h); frame.draw(c); c.drawBitmap(src, null, new RectF(10, 10, image_w-10, image_h-10), p); return result; } else if(mi.type == 2){ Bitmap src = ThumbnailUtils.createVideoThumbnail(mi.path, MediaStore.Video.Thumbnails.MINI_KIND); float aspect = (float)src.getWidth()/src.getHeight(); int image_w = 160; int image_h = (int)((image_w-20)/aspect+20); Bitmap result = Bitmap.createBitmap(image_w, image_h, Config.ARGB_8888); Canvas c = new Canvas(result); frame.setBounds(0, 0, image_w, image_h); frame.draw(c); c.drawBitmap(src, null, new RectF(10, 10, image_w-10, image_h-10), p); c.drawBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.video), (image_w-64)/2, (image_h-64)/2, p); return result; } } catch(Exception e){ return null; } catch(OutOfMemoryError e){ return null; } return null; } Bitmap getBmp(int res){ Bitmap bmp = Bitmap.createBitmap(160, 140, Config.ARGB_8888); Canvas c = new Canvas(bmp); Paint p = new Paint(); p.setDither(true); p.setFilterBitmap(true); frame.setBounds(0, 0, 160, 140); frame.draw(c); c.drawBitmap(BitmapFactory.decodeResource(getResources(), res), 48, 38, p); return bmp; } public class MediaItem { String path; String date; int type; public MediaItem(String str1, String str2, int i){ path = str1; date = str2; type = i; } } public class Item { String path; Bitmap thumbnail; int type; public Item(String str, Bitmap bmp, int i){ path = str; thumbnail = bmp; type = i; } } public class Transformation { Matrix matrix3d; int alpha; public Transformation(int pos, float percent, int imageWidth, int imageHeight){ float centerX = (float)width/2; float centerY = (float)height/2; float f = -pos + percent; centerY -= (float)Math.sin(f*Math.PI/2)*40; float f1 = Math.abs(f) - 0.5f; if(f1 < 0) f1 = 0; float f2 = Math.abs(f) / 1.5f; alpha = 255 - (int)(255*f1); float scale = (float) (1 - 0.4*f2); Camera c = new Camera(); matrix3d = new Matrix(); c.save(); float rotate = percent*100; if(pos == 0) rotate *= -1; c.rotateX(rotate); c.getMatrix(matrix3d); matrix3d.preTranslate(-imageWidth/2, -imageHeight/2); matrix3d.postScale(scale, scale); matrix3d.postTranslate(centerX, centerY); c.restore(); } } private class GestureListener extends GestureDetector.SimpleOnGestureListener { @Override public boolean onFling(MotionEvent me1, MotionEvent me2, float vX, float vY){ scroller.fling(0, getScrollY, 0, -(int)vY, 0, 0, -50, Math.max(items.length-1, 0)*140+50); invalidate(); return true; } } }
      
      







少し説明:



ContentObserverは、すべての変更を外部メモリに記録します。 たとえば、写真を追加すると、onChange(boolean selfChange)メソッドが数ミリ秒の間隔で数回呼び出されます。 loadThumbnails()メソッドは、すべての写真のサムネイルを個別のストリームにロードします。 onChange ContentObserverメソッドがトリガーされるたびに、サムネイルを作成するための新しいストリームが作成されます。 1つの写真を追加するときに、同じアクションを実行して複数のストリームが同時に同時に作成されると、状況が発生します。 この状況を回避するために、最後に作成されたストリームのIDはloaderId変数に格納され、すべてのフローで、格納されたIDとこのストリームのIDの比較があります。 数値が等しくない場合、ストリームは破棄されます。



通常のように、フリックジェスチャーとスロージェスチャーによって、ビューキャンバスが移動することはありません。 代わりに、getScrollY変数が変更されます。 整数のgetSelected()は、サムネイル配列内の現在の位置を返し、float getSelectedPercent()は、ビューアーに対するサムネイルの角度を返します。 0.5の値は45度に対応します。 おそらく次の図は状況を明確にするでしょう:







onTouchEvent(MotionEvent me)メソッドでは、getParent()。RequestDisallowInterceptTouchEvent(true)を呼び出すことを忘れないようにすることが重要です。これにより、ウィジェットのサムネイルをスクロールするときに、ランチャーのテーブルがスクロールしなくなります。



最後に、トランスフォーメーションクラスについて少し説明します。 上記のgetSelected()およびgetSelectedPercent()が返すものに応じて、サムネイルの変換行列を返します。



また、すべての画像と動画のサムネイルがItem []配列に保存されることにも注意してください。 これは悪いです。 適切なオプションは、サムネイルキャッシングです。



おわりに



素敵な写真のサムネイルビューアーを作成する方法について簡単に説明しました。 私の昔のSamsung Galaxy Gioの携帯電話では、画像の反転のような美しい3次元効果が遅くならないことに非常に驚いています。 これはビデオで見ることができます:






All Articles