パンダのメモ帳

技術系のネタをゆるゆると

CentOS 5 に Node.js と npm をインストールする

Mac だと Homebrew なんかで簡単にインストールできたんだけど、CentOS の場合まだ yum じゃインストールできなかった。そこで色々な手順を試してみて一番しっくりきた手順をメモ。

Node.js をインストール

git を使う方法nave を導入する方法 など、色々な手順が解説されているけれども、サーバーサイドに普通にインストールする場合、結局公式のソースをダウンロードしてコンパイルするのがいいとの結論に達した。以下手順。

[root@localhost ~]# cd /usr/local/src
[root@localhost src]# wget http://nodejs.org/dist/v0.6.11/node-v0.6.11.tar.gz
[root@localhost src]# tar xf node-v0.6.11.tar.gz
[root@localhost src]# cd node-v0.6.11
[root@localhost node-v0.6.11]# ./configure
[root@localhost node-v0.6.11]# make install

上記手順で /usr/local/bin/node がインストールされる。今回の最新安定版は v0.6.11 だった。

npm もインストール

npm は Node.js 向けのパッケージマネージャ。Node.js を使って開発をする上で入れない理由がないのでとっととインストール。

[root@localhost ~]# curl http://npmjs.org/install.sh | sh

上記手順で /usr/local/bin/npm がインストールされる。

バージョン確認

[root@localhost ~]# node --version
v0.6.11
[root@localhost ~]# npm --version
1.1.1

CentOS 5.5 で MTU の値を変更する

CentOS 5.5 で MTU(Maximum Transmission Unit)を変更する方法について

一時的、または即座に変更したい場合

次のコマンドを実行する。

[root@localhost ~]# ifconfig <インターフェース名> mtu <MTU値>

eth0 のMTU値を 1454 に変更したい場合は次の通り。

[root@localhost ~]# ifconfig eth0 mtu 1454

これで即座にMTU値が変更されるが、OSまたはネットワークの再起動を行った場合、デフォルト値に戻ってしまう。恒久的に変更したい場合は次の手順を踏む必要がある。

恒久的に変更したい場合

ネットワークインターフェースの設定ファイル(初期化スクリプト)に次の1行を加える。ただし、この方法では即座に反映されるわけではないので、すぐに変更したい場合は前述の ifconfig コマンドによる方法と併用する必要がある。

MTU=<MTU値>

eth0 のMTU値を 1454 に変更したい場合は次の通り。

MTU=1454

設定ファイルの場所は

/etc/sysconfig/network-scripts/ifcfg-<インターフェース名>

なので、eth0 の場合は

/etc/sysconfig/network-scripts/ifcfg-eth0

となる。

なぜMTU値を変更するのか?

CentOS 5.5 の場合、MTU値のデフォルトは 1500。この値はイーサネットに最適な値とされており、最近のブロードバンド環境においてもこの値であまり問題ないとされている。……が、先日 Postfix で特定のホストからのメールのみ受信できないという問題が発生した。ログを確認すると次のようなエラーメッセージが確認された。

timeout after DATA from foo.example.com[xxx.xx.xx.xxx]

調べてみると、メールのヘッダ+本文が一定のサイズ以上になった場合に同メッセージを吐いて受信に失敗している模様。そこでぐぐってみると以下のようなスレッドを発見した。

スレッドを追ってみると、どうやらMTU値が適正に設定されていないのでは?ということでさらに調べてみると次のページを発見。

利用しているネットワークがBフレッツの場合、どうやらMTU値は 1454 に設定するべし、ということらしい。そこで今回紹介した手順でMTU値を変更してみたところ、無事にメールが受信できたというわけだ。
同じような問題に困っているネットワーク管理者の人はぜひ試してみて欲しい。

ドラッグ&ドロップで並び替えできる ListView

前書き

電話帳Rで「グループの並び替え」を実現するインターフェースを検討していたところ、id:vvakame 氏がコードを公開してくれていた。

ユーザがソート可能なListView - @vvakame の日記
http://d.hatena.ne.jp/vvakame/20100718#1279453854

さらに、このコードを元に id:tomorrowkey 氏がカスタマイズしたものも発見。

ユーザがソート可能なListViewをすこしリッチにしてみた - 明日の鍵
http://d.hatena.ne.jp/tomorrowkey/20100720/1279597322

これらのコードをそのままいただいても良さそうだったんだけど、何をしているのか理解していないコードをそのまま使うのは微妙だし……ってことで整理しつつコードレビューしてたら殆ど別物ができました、っていうお話。

変更点

全体的に「単一の ListView で要素を並び替える」機能だけに特化してます。

  • リスト要素が捨てられる機能をバッサリ削除
    • 今回は必要がなかったため。並び替えだけしたいって需要があると思った。
  • ドラッグの開始を onTouch から onItemLongClick に変更
    • 長押しするまでは普通の ListView と同じようにスクロールできるように。
  • その他
    • 気づいたらなんかすごく別物感が。

前述のお二人のコードはもう少し機能的にリッチなカンジです。

ソースコード & Sample

いかんせん、Java歴が短いのでなにか変なことしてるかも。ツッコミはコメントか twitter でよろしくです。

res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <jp.ne.hatena.d.shogo0809.widget.SortableListView
        android:id="@+id/list"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" />
</LinearLayout>
SampleActivity.java
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;

import jp.ne.hatena.d.shogo0809.widget.SortableListView;

public class SampleActivity extends Activity {
    static final String[] PREFS = { "北海道", "青森県", "岩手県", "宮城県", "秋田県", "山形県",
            "福島県", "茨城県", "栃木県", "群馬県", "埼玉県", "千葉県", "東京都", "神奈川県", "新潟県",
            "富山県", "石川県", "福井県", "山梨県", "長野県", "岐阜県", "静岡県", "愛知県", "三重県",
            "滋賀県", "京都府", "大阪府", "兵庫県", "奈良県", "和歌山県", "鳥取県", "島根県", "岡山県",
            "広島県", "山口県", "徳島県", "香川県", "愛媛県", "高知県", "福岡県", "佐賀県", "長崎県",
            "熊本県", "大分県", "宮崎県", "鹿児島県", "沖縄県" };
    
    int mDraggingPosition = -1;
    SampleAdapter mAdapter;
    SortableListView mListView;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        mAdapter = new SampleAdapter();
        mListView = (SortableListView) findViewById(R.id.list);
        mListView.setDragListener(new DragListener());
        mListView.setSortable(true);
        mListView.setAdapter(mAdapter);
    }
    
    class SampleAdapter extends BaseAdapter {
        @Override
        public int getCount() {
            return PREFS.length;
        }
        
        @Override
        public String getItem(int position) {
            return PREFS[position];
        }
        
        @Override
        public long getItemId(int position) {
            return position;
        }
        
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            if (convertView == null) {
                convertView = getLayoutInflater().inflate(
                        android.R.layout.simple_list_item_1, null);
            }
            final TextView view = (TextView) convertView;
            view.setText(PREFS[position]);
            view.setVisibility(position == mDraggingPosition ? View.INVISIBLE
                    : View.VISIBLE);
            return convertView;
        }
    }
    
    class DragListener extends SortableListView.SimpleDragListener {
        @Override
        public int onStartDrag(int position) {
            mDraggingPosition = position;
            mListView.invalidateViews();
            return position;
        }
        
        @Override
        public int onDuringDrag(int positionFrom, int positionTo) {
            if (positionFrom < 0 || positionTo < 0
                    || positionFrom == positionTo) {
                return positionFrom;
            }
            int i;
            if (positionFrom < positionTo) {
                final int min = positionFrom;
                final int max = positionTo;
                final String data = PREFS[min];
                i = min;
                while (i < max) {
                    PREFS[i] = PREFS[++i];
                }
                PREFS[max] = data;
            } else if (positionFrom > positionTo) {
                final int min = positionTo;
                final int max = positionFrom;
                final String data = PREFS[max];
                i = max;
                while (i > min) {
                    PREFS[i] = PREFS[--i];
                }
                PREFS[min] = data;
            }
            mDraggingPosition = positionTo;
            mListView.invalidateViews();
            return positionTo;
        }
        
        @Override
        public boolean onStopDrag(int positionFrom, int positionTo) {
            mDraggingPosition = -1;
            mListView.invalidateViews();
            return super.onStopDrag(positionFrom, positionTo);
        }
    }
}
SortableListView.java
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.PixelFormat;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemLongClickListener;
import android.widget.ImageView;
import android.widget.ListView;

public class SortableListView extends ListView implements
        OnItemLongClickListener {
    private static final int SCROLL_SPEED_FAST = 25;
    private static final int SCROLL_SPEED_SLOW = 8;
    private static final Bitmap.Config DRAG_BITMAP_CONFIG = Bitmap.Config.ARGB_8888;
    
    private boolean mSortable = false;
    private boolean mDragging = false;
    private DragListener mDragListener = new SimpleDragListener();
    private int mBitmapBackgroundColor = Color.argb(128, 0xFF, 0xFF, 0xFF);
    private Bitmap mDragBitmap = null;
    private ImageView mDragImageView = null;
    private WindowManager.LayoutParams mLayoutParams = null;
    private MotionEvent mActionDownEvent;
    private int mPositionFrom = -1;
    
    /** コンストラクタ */
    public SortableListView(Context context) {
        super(context);
        setOnItemLongClickListener(this);
    }
    
    /** コンストラクタ */
    public SortableListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setOnItemLongClickListener(this);
    }
    
    /** コンストラクタ */
    public SortableListView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        setOnItemLongClickListener(this);
    }
    
    /** ドラッグイベントリスナの設定 */
    public void setDragListener(DragListener listener) {
        mDragListener = listener;
    }
    
    /** ソートモードの切替 */
    public void setSortable(boolean sortable) {
        this.mSortable = sortable;
    }
    
    /** ソート中アイテムの背景色を設定 */
    @Override
    public void setBackgroundColor(int color) {
        mBitmapBackgroundColor = color;
    }
    
    /** ソートモードの設定 */
    public boolean getSortable() {
        return mSortable;
    }
    
    /** MotionEvent から position を取得する */
    private int eventToPosition(MotionEvent event) {
        return pointToPosition((int) event.getX(), (int) event.getY());
    }
    
    /** タッチイベント処理 */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (!mSortable) {
            return super.onTouchEvent(event);
        }
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                storeMotionEvent(event);
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                if (duringDrag(event)) {
                    return true;
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                if (stopDrag(event, true)) {
                    return true;
                }
                break;
            }
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_OUTSIDE: {
                if (stopDrag(event, false)) {
                    return true;
                }
                break;
            }
        }
        return super.onTouchEvent(event);
    }
    
    /** リスト要素長押しイベント処理 */
    @Override
    public boolean onItemLongClick(AdapterView<?> parent, View view,
            int position, long id) {
        return startDrag();
    }
    
    /** ACTION_DOWN 時の MotionEvent をプロパティに格納 */
    private void storeMotionEvent(MotionEvent event) {
        mActionDownEvent = event;
    }
    
    /** ドラッグ開始 */
    private boolean startDrag() {
        // イベントから position を取得
        mPositionFrom = eventToPosition(mActionDownEvent);
        
        // 取得した position が 0未満=範囲外の場合はドラッグを開始しない
        if (mPositionFrom < 0) {
            return false;
        }
        mDragging = true;
        
        // View, Canvas, WindowManager の取得・生成
        final View view = getChildByIndex(mPositionFrom);
        final Canvas canvas = new Canvas();
        final WindowManager wm = getWindowManager();
        
        // ドラッグ対象要素の View を Canvas に描画
        mDragBitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(),
                DRAG_BITMAP_CONFIG);
        canvas.setBitmap(mDragBitmap);
        view.draw(canvas);
        
        // 前回使用した ImageView が残っている場合は除去(念のため?)
        if (mDragImageView != null) {
            wm.removeView(mDragImageView);
        }
        
        // ImageView 用の LayoutParams が未設定の場合は設定する
        if (mLayoutParams == null) {
            initLayoutParams();
        }
        
        // ImageView を生成し WindowManager に addChild する
        mDragImageView = new ImageView(getContext());
        mDragImageView.setBackgroundColor(mBitmapBackgroundColor);
        mDragImageView.setImageBitmap(mDragBitmap);
        wm.addView(mDragImageView, mLayoutParams);
        
        // ドラッグ開始
        if (mDragListener != null) {
            mPositionFrom = mDragListener.onStartDrag(mPositionFrom);
        }
        return duringDrag(mActionDownEvent);
    }
    
    /** ドラッグ処理 */
    private boolean duringDrag(MotionEvent event) {
        if (!mDragging || mDragImageView == null) {
            return false;
        }
        final int x = (int) event.getX();
        final int y = (int) event.getY();
        final int height = getHeight();
        final int middle = height / 2;
        
        // スクロール速度の決定
        final int speed;
        final int fastBound = height / 9;
        final int slowBound = height / 4;
        if (event.getEventTime() - event.getDownTime() < 500) {
            // ドラッグの開始から500ミリ秒の間はスクロールしない
            speed = 0;
        } else if (y < slowBound) {
            speed = y < fastBound ? -SCROLL_SPEED_FAST : -SCROLL_SPEED_SLOW;
        } else if (y > height - slowBound) {
            speed = y > height - fastBound ? SCROLL_SPEED_FAST
                    : SCROLL_SPEED_SLOW;
        } else {
            speed = 0;
        }
        
        // スクロール処理
        if (speed != 0) {
            // 横方向はとりあえず考えない
            int middlePosition = pointToPosition(0, middle);
            if (middlePosition == AdapterView.INVALID_POSITION) {
                middlePosition = pointToPosition(0, middle + getDividerHeight()
                        + 64);
            }
            final View middleView = getChildByIndex(middlePosition);
            if (middleView != null) {
                setSelectionFromTop(middlePosition, middleView.getTop() - speed);
            }
        }
        
        // ImageView の表示や位置を更新
        if (mDragImageView.getHeight() < 0) {
            mDragImageView.setVisibility(View.INVISIBLE);
        } else {
            mDragImageView.setVisibility(View.VISIBLE);
        }
        updateLayoutParams(x, y);
        getWindowManager().updateViewLayout(mDragImageView, mLayoutParams);
        if (mDragListener != null) {
            mPositionFrom = mDragListener.onDuringDrag(mPositionFrom,
                    pointToPosition(x, y));
        }
        return true;
    }
    
    /** ドラッグ終了 */
    private boolean stopDrag(MotionEvent event, boolean isDrop) {
        if (!mDragging) {
            return false;
        }
        if (isDrop && mDragListener != null) {
            mDragListener.onStopDrag(mPositionFrom, eventToPosition(event));
        }
        mDragging = false;
        if (mDragImageView != null) {
            getWindowManager().removeView(mDragImageView);
            mDragImageView = null;
            // リサイクルするとたまに死ぬけどタイミング分からない by vvakame
            // mDragBitmap.recycle();
            mDragBitmap = null;
            return true;
        }
        return false;
    }
    
    /** 指定インデックスのView要素を取得する */
    private View getChildByIndex(int index) {
        return getChildAt(index - getFirstVisiblePosition());
    }
    
    /** WindowManager の取得 */
    protected WindowManager getWindowManager() {
        return (WindowManager) getContext().getSystemService(
                Context.WINDOW_SERVICE);
    }
    
    /** ImageView 用 LayoutParams の初期化 */
    protected void initLayoutParams() {
        mLayoutParams = new WindowManager.LayoutParams();
        mLayoutParams.gravity = Gravity.TOP | Gravity.LEFT;
        mLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
        mLayoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
        mLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
                | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
                | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
        mLayoutParams.format = PixelFormat.TRANSLUCENT;
        mLayoutParams.windowAnimations = 0;
        mLayoutParams.x = getLeft();
        mLayoutParams.y = getTop();
    }
    
    /** ImageView 用 LayoutParams の座標情報を更新 */
    protected void updateLayoutParams(int x, int y) {
        mLayoutParams.y = getTop() + y - 32;
    }
    
    /** ドラッグイベントリスナーインターフェース */
    public interface DragListener {
        /** ドラッグ開始時の処理 */
        public int onStartDrag(int position);
        
        /** ドラッグ中の処理 */
        public int onDuringDrag(int positionFrom, int positionTo);
        
        /** ドラッグ終了=ドロップ時の処理 */
        public boolean onStopDrag(int positionFrom, int positionTo);
    }
    
    /** ドラッグイベントリスナー実装 */
    public static class SimpleDragListener implements DragListener {
        /** ドラッグ開始時の処理 */
        @Override
        public int onStartDrag(int position) {
            return position;
        }
        
        /** ドラッグ中の処理 */
        @Override
        public int onDuringDrag(int positionFrom, int positionTo) {
            return positionFrom;
        }
        
        /** ドラッグ終了=ドロップ時の処理 */
        @Override
        public boolean onStopDrag(int positionFrom, int positionTo) {
            return positionFrom != positionTo && positionFrom >= 0
                    || positionTo >= 0;
        }
    }
}

サポート用サイトを開設しました

「電話帳R」をはじめとしたアンドロイド向けアプリのサポート用サイトを開設しました。

studiofly apps

公開しているアプリの解説や、サポート用の掲示板も用意しています。
今後、アンドロイドアプリに関するご意見などは上記のサイトをご利用ください。